「二哥,要不我上大學的時候也學習編程吧?」有一天,三妹突發奇想地問我。html
「你肯定要作一名程序媛嗎?」java
「我以爲女生作程序員,有着天大的優點,尤爲是我這種長相甜美的。」三妹開始認真了起來。程序員
「好像是啊,遇到女生提問,我好像一直蠻熱情的。」shell
「二哥,你不是愛好寫做嘛,仍是一個 Java 程序員,不妨寫個專欄,名字就叫《教妹學 Java》。我高考完就開始跟着你學習編程,還能省下一筆培訓費。」三妹看起來已經替我籌劃好了呀。編程
「真的很服氣大家零零後,蠻有想法的。恰好我最近在寫 Java 系列的專欄,不妨試一試!」數組
PS:親愛的讀者朋友們,咱們今天就從晦澀難懂的「泛型」開始吧!(子標題是三妹提出來的,內容由二哥我來回答)安全
三妹啊,聽哥慢慢給你講啊。app
Java 在 5.0 時增長了泛型機制,聽說專家們爲此花費了 5 年左右的時間(聽起來很不容易)。有了泛型以後,尤爲是對集合類的使用,就變得更規範了。工具
看下面這段簡單的代碼。學習
ArrayList<String> list = new ArrayList<String>();
list.add("沉默王二");
String str = list.get(0);
複製代碼
但在沒有泛型以前該怎麼辦呢?
首先,咱們須要使用 Object 數組來設計 Arraylist
類。
class Arraylist {
private Object[] objs;
private int i = 0;
public void add(Object obj) {
objs[i++] = obj;
}
public Object get(int i) {
return objs[i];
}
}
複製代碼
而後,咱們向 Arraylist
中存取數據。
Arraylist list = new Arraylist();
list.add("沉默王二");
list.add(new Date());
String str = (String)list.get(0);
複製代碼
你有沒有發現兩個問題:
對比一下,你就能明顯地感覺到泛型的優秀之處:使用類型參數解決了元素的不肯定性——參數類型爲 String 的集合中是不容許存放其餘類型元素的,取出數據的時候也不須要強制類型轉換了。
三妹啊,你一個小白只要會用泛型就好了,還想設計泛型啊?!不過,既然你想了解,那麼哥責無旁貸。
首先,咱們來按照泛型的標準從新設計一下 Arraylist
類。
class Arraylist<E> {
private Object[] elementData;
private int size = 0;
public Arraylist(int initialCapacity) {
this.elementData = new Object[initialCapacity];
}
public boolean add(E e) {
elementData[size++] = e;
return true;
}
E elementData(int index) {
return (E) elementData[index];
}
}
複製代碼
一個泛型類就是具備一個或多個類型變量的類。Arraylist 類引入的類型變量爲 E(Element,元素的首字母),使用尖括號 <>
括起來,放在類名的後面。
而後,咱們能夠用具體的類型(好比字符串)替換類型變量來實例化泛型類。
Arraylist<String> list = new Arraylist<String>();
list.add("沉默王三");
String str = list.get(0);
複製代碼
Date 類型也能夠的。
Arraylist<Date> list = new Arraylist<Date>();
list.add(new Date());
Date date = list.get(0);
複製代碼
其次,咱們還能夠在一個非泛型的類(或者泛型類)中定義泛型方法。
class Arraylist<E> {
public <T> T[] toArray(T[] a) {
return (T[]) Arrays.copyOf(elementData, size, a.getClass());
}
}
複製代碼
不過,說實話,泛型方法的定義看起來略顯晦澀。來一副圖吧(注意:方法返回類型和方法參數類型至少須要一個)。
如今,咱們來調用一下泛型方法。
Arraylist<String> list = new Arraylist<>(4);
list.add("沉");
list.add("默");
list.add("王");
list.add("二");
String [] strs = new String [4];
strs = list.toArray(strs);
for (String str : strs) {
System.out.println(str);
}
複製代碼
最後,咱們再來講說泛型變量的限定符 extends
。在解釋這個限定符以前,咱們假設有三個類,它們之間的定義是這樣的。
class Wanglaoer {
public String toString() {
return "王老二";
}
}
class Wanger extends Wanglaoer{
public String toString() {
return "王二";
}
}
class Wangxiaoer extends Wanger{
public String toString() {
return "王小二";
}
}
複製代碼
咱們使用限定符 extends
來從新設計一下 Arraylist
類。
class Arraylist<E extends Wanger> {
}
複製代碼
當咱們向 Arraylist
中添加 Wanglaoer
元素的時候,編譯器會提示錯誤:Arraylist
只容許添加 Wanger
及其子類 Wangxiaoer
對象,不容許添加其父類 Wanglaoer
。
Arraylist<Wanger> list = new Arraylist<>(3);
list.add(new Wanger());
list.add(new Wanglaoer());
// The method add(Wanger) in the type Arraylist<Wanger> is not applicable for the arguments
// (Wanglaoer)
list.add(new Wangxiaoer());
複製代碼
也就是說,限定符 extends
能夠縮小泛型的類型範圍。
三妹,你功課作得能夠啊,連虛擬機都知道了啊。哥能夠確定地回答你,虛擬機是沒有泛型的。
囉嗦一句哈。咱們編寫的 Java 代碼(也就是源碼,後綴爲 .java 的文件)是不可以被操做系統直接識別的,須要先編譯,生成 .class 文件(也就是字節碼文件)。而後 Java 虛擬機(JVM)會充當一個翻譯官的角色,把字節碼翻譯給操做系統能聽得懂的語言,告訴它該幹嗎。
怎麼肯定虛擬機沒有泛型呢?咱們須要把泛型類的字節碼進行反編譯——強烈推薦超神反編譯工具 Jad !
如今,在命令行中敲如下代碼吧(反編譯 Arraylist
的字節碼文件 Arraylist.class
)。
jad Arraylist.class
複製代碼
命令執行完後,會生成一個 Arraylist.jad 的文件,用文本編輯工具打開後的結果以下。
// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3)
// Source File Name: Arraylist.java
package com.cmower.java_demo.fanxing;
import java.util.Arrays;
class Arraylist {
public Arraylist(int initialCapacity) {
size = 0;
elementData = new Object[initialCapacity];
}
public boolean add(Object e) {
elementData[size++] = e;
return true;
}
Object elementData(int index) {
return elementData[index];
}
private Object elementData[];
private int size;
}
複製代碼
類型變量 <E>
消失了,取而代之的是 Object !
既然如此,那若是泛型類使用了限定符 extends
,結果會怎麼樣呢?咱們先來看看 Arraylist2
的源碼。
class Arraylist2<E extends Wanger> {
private Object[] elementData;
private int size = 0;
public Arraylist2(int initialCapacity) {
this.elementData = new Object[initialCapacity];
}
public boolean add(E e) {
elementData[size++] = e;
return true;
}
E elementData(int index) {
return (E) elementData[index];
}
}
複製代碼
字節碼文件 Arraylist2.class
使用 Jad 反編譯後的結果以下。
// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3)
// Source File Name: Arraylist2.java
package com.cmower.java_demo.fanxing;
// Referenced classes of package com.cmower.java_demo.fanxing:
// Wanger
class Arraylist2 {
public Arraylist2(int initialCapacity) {
size = 0;
elementData = new Object[initialCapacity];
}
public boolean add(Wanger e) {
elementData[size++] = e;
return true;
}
Wanger elementData(int index) {
return (Wanger)elementData[index];
}
private Object elementData[];
private int size;
}
複製代碼
類型變量 <E extends Wanger>
不見了,E 被替換成了 Wanger
。
經過以上兩個例子說明,Java 虛擬機會將泛型的類型變量擦除,並替換爲限定類型(沒有限定的話,就用 Object
)。
三妹啊,你還別說,類型擦除真的會有一些「問題」。
咱們來看一下這段代碼。
public class Cmower {
public static void method(Arraylist<String> list) {
System.out.println("Arraylist<String> list");
}
public static void method(Arraylist<Date> list) {
System.out.println("Arraylist<Date> list");
}
}
複製代碼
在淺層的意識上,咱們會想固然地認爲 Arraylist<String> list
和 Arraylist<Date> list
是兩種不一樣的類型,由於 String 和 Date 是不一樣的類。
但因爲類型擦除的緣由,以上代碼是不會經過編譯的——編譯器會提示一個錯誤(這正是類型擦除引起的那些「問題」):
Erasure of method method(Arraylist) is the same as another method in type Cmower
Erasure of method method(Arraylist) is the same as another method in type Cmower
大體的意思就是,這兩個方法的參數類型在擦除後是相同的。
也就是說,method(Arraylist<String> list)
和 method(Arraylist<Date> list)
是同一種參數類型的方法,不能同時存在。類型變量 String
和 Date
在擦除後會自動消失,method 方法的實際參數是 Arraylist list
。
有句俗話叫作:「百聞不如一見」,但即便見到了也未必爲真——泛型的擦除問題就能夠很好地佐證這個觀點。
三妹啊,哥忽然以爲你很適合做一枚可愛的程序媛啊!你這預習的功課作得可真到家啊,連通配符都知道!
通配符使用英文的問號(?)來表示。在咱們建立一個泛型對象時,可使用關鍵字 extends
限定子類,也可使用關鍵字 super
限定父類。
爲了更好地解釋通配符,咱們須要對 Arraylist
進行一些改進。
class Arraylist<E> {
private Object[] elementData;
private int size = 0;
public Arraylist(int initialCapacity) {
this.elementData = new Object[initialCapacity];
}
public boolean add(E e) {
elementData[size++] = e;
return true;
}
public E get(int index) {
return (E) elementData[index];
}
public int indexOf(Object o) {
if (o == null) {
for (int i = 0; i < size; i++)
if (elementData[i]==null)
return i;
} else {
for (int i = 0; i < size; i++)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
public boolean contains(Object o) {
return indexOf(o) >= 0;
}
public String toString() {
StringBuilder sb = new StringBuilder();
for (Object o : elementData) {
if (o != null) {
E e = (E)o;
sb.append(e.toString());
sb.append(',').append(' ');
}
}
return sb.toString();
}
public int size() {
return size;
}
public E set(int index, E element) {
E oldValue = (E) elementData[index];
elementData[index] = element;
return oldValue;
}
}
複製代碼
1)新增 indexOf(Object o)
方法,判斷元素在 Arraylist
中的位置。注意參數爲 Object
而不是泛型 E
。
2)新增 contains(Object o)
方法,判斷元素是否在 Arraylist
中。注意參數爲 Object
而不是泛型 E
。
3)新增 toString()
方法,方便對 Arraylist
進行打印。
4)新增 set(int index, E element)
方法,方便對 Arraylist
元素的更改。
你知道,Arraylist<Wanger> list = new Arraylist<Wangxiaoer>();
這樣的語句是沒法經過編譯的,儘管 Wangxiaoer 是 Wanger 的子類。但若是咱們確實須要這種 「向上轉型」 的關係,該怎麼辦呢?這時候就須要通配符來發揮做用了。
利用 <? extends Wanger>
形式的通配符,能夠實現泛型的向上轉型,來看例子。
Arraylist<? extends Wanger> list2 = new Arraylist<>(4);
list2.add(null);
// list2.add(new Wanger());
// list2.add(new Wangxiaoer());
Wanger w2 = list2.get(0);
// Wangxiaoer w3 = list2.get(1);
複製代碼
list2 的類型是 Arraylist<? extends Wanger>
,翻譯一下就是,list2 是一個 Arraylist
,其類型是 Wanger
及其子類。
注意,「關鍵」來了!list2 並不容許經過 add(E e)
方法向其添加 Wanger
或者 Wangxiaoer
的對象,惟一例外的是 null
。爲何不能存呢?緣由還有待探究(苦澀)。
那就奇了怪了,既然不讓存放元素,那要 Arraylist<? extends Wanger>
這樣的 list2 有什麼用呢?
雖然不能經過 add(E e)
方法往 list2 中添加元素,但能夠給它賦值。
Arraylist<Wanger> list = new Arraylist<>(4);
Wanger wanger = new Wanger();
list.add(wanger);
Wangxiaoer wangxiaoer = new Wangxiaoer();
list.add(wangxiaoer);
Arraylist<? extends Wanger> list2 = list;
Wanger w2 = list2.get(1);
System.out.println(w2);
System.out.println(list2.indexOf(wanger));
System.out.println(list2.contains(new Wangxiaoer()));
複製代碼
Arraylist<? extends Wanger> list2 = list;
語句把 list 的值賦予了 list2,此時 list2 == list
。因爲 list2 不容許往其添加其餘元素,因此此時它是安全的——咱們能夠從容地對 list2 進行 get()
、indexOf()
和 contains()
。想想,若是能夠向 list2 添加元素的話,這 3 個方法反而變得不太安全,它們的值可能就會變。
利用 <? super Wanger>
形式的通配符,能夠向 Arraylist 中存入父類是 Wanger
的元素,來看例子。
Arraylist<? super Wanger> list3 = new Arraylist<>(4);
list3.add(new Wanger());
list3.add(new Wangxiaoer());
// Wanger w3 = list3.get(0);
複製代碼
須要注意的是,沒法從 Arraylist<? super Wanger>
這樣類型的 list3 中取出數據。爲何不能取呢?緣由還有待探究(再次苦澀)。
雖然緣由有待探究,但結論是明確的:<? extends T>
能夠取數據,<? super T>
能夠存數據。那麼利用這一點,咱們就能夠實現數組的拷貝——<? extends T>
做爲源(保證源不會發生變化),<? super T>
做爲目標(能夠保存值)。
public class Collections {
public static <T> void copy(Arraylist<? super T> dest, Arraylist<? extends T> src) {
for (int i = 0; i < src.size(); i++)
dest.set(i, src.get(i));
}
}
複製代碼
「二哥,你今天苦澀了啊!嘿嘿。居然還有你須要探究的。」三妹開始調皮了起來。
「......」
「不要很差意思嘛,等三妹啥時候探究出來了緣由,三妹給你講,好很差?」三妹越說越來勁了。
「......」
「二哥,你還在想泛型通配符的緣由啊!那三妹先去預習下個知識點了啊,你思考完了,再給我講!」三妹看着我陷入了沉思,扔下這句話走了。
「......」