泛型實現參數化類型的概念,使代碼可以應用於多種類型,解除類或方法與所使用的類型之間的約束。在JDK 1.5開始引入了泛型,但Java實現泛型的方式與C++或C#差異很大。在平常寫代碼用到泛型時,彷彿一切都來得如此理所當然。但其實Java泛型還是有挺多tricky的東西的,編譯器在背後爲我們做了很多事。下面我們來看看有關Java泛型容易忽視的點。
泛型不支持協變
什麼是協變?舉個例子。
class Fruit{} class Apple extends Fruit{} Fruit[] fruit = new Apple[10]; // OK123123
子類數組可以賦給父類數組的引用。但泛型是不支持這種協變的。
ArrayList<Fruit> flist = new ArrayList<Apple>(); // 無法通過編譯11
但我們可以使用通配符來解決
ArrayList<? extends Fruit> flist = new ArrayList<Apple>();// 使用通配符解決協變問題11
通配符
上界通配符
List<? extends Fruit> flist = Arrays.asList(new Apple()); Apple a = (Apple)flist.get(0); // No warning flist.contains(new Apple()); // Argument is ‘Object’ flist.indexOf(new Apple()); // Argument is ‘Object’ //flist.add(new Apple()); 無法編譯1234512345
List<? extends Fruit>
表示某種特定類型 ( Fruit 或者其子類 ) 的 List,但是編譯器並不關心(不知道)這個實際的具體類型到底是什麼。值得注意的是,這並不意味着這個List可以持有Fruit的任意類型!
由於List的具體類型是並不確定的,而且Java泛型是不支持協變的,因此帶有泛型類型參數的方法都無法正常調用。比如add(T item);
,即使是傳Object也無法通過編譯。
但對於返回類型是泛型的方法,比如T get(int index);
,返回值類型與上界類型一樣。如上面示例代碼調用的flist.get(0)
返回值就是Fruit類型的。
下界通配符
static void add(List<? super Apple> list) { // list.add(new Fruit()); // 無法編譯 Object object = list.get(0);// pass }12341234
代碼中的 List<? super Apple> list
表明list持有的類型是Apple的父類類型,但與上界通配符類似,這並不意味list可以持有Apple任意的子類類型的對象,編譯器並不知道list具體的類型是什麼。因此,list.add(new Fruit());
就不能編譯了。
***通配符
List<?> list
表示 list
是持有某種特定類型的 List,但是不知道具體是哪種類型。而單獨的 List list
,也就是沒有傳入泛型參數,表示這個 list 持有的元素的類型是 Object
。
所有泛型信息都被擦除了嗎
所謂的擦除,僅僅是對方法的Code屬性中的字節碼(也就是方法內的邏輯代碼)進行擦除,實際上元數據(類和接口的聲明,類字段的聲明)中還是保留了泛型信息。
引用R大的話就是:
位於聲明一側的,源碼裏寫了什麼到運行時就能看到什麼;
位於使用一側的,源碼裏寫什麼到運行時都沒了。
public class GenericClass<T> { // 1 private List<T> list; // 2 private Map<String, T> map; // 3 public <U> U genericMethod(Map<T, U> m) { // 4 List<String> list = new ArrayList<>(); // 5 return null; } } 1234567812345678
上面的代碼中,註釋1到註釋4的T和U是保留在Class文件當中的,源碼是什麼,那麼通過反射獲取得到的就是什麼。也就是說,在運行時,是無法獲取到具體的T和U是什麼類型的。
但運行時,在方法內部的局部變量的泛型信息是被全部擦除的。如上的註釋5中的list的具體類型是無法在運行時獲取到的。
真的無法獲取到泛型類型嗎
當時今日頭條的面試官問過我這個問題,我當時對泛型的認識比較淺薄,以爲編譯器會將所有的泛型信息擦除,那麼運行時也就無能獲取到具體的泛型類型了。但其實並不是這樣,如上面介紹到,JDK1.5之後,Class的格式有變化,編譯器會將聲明的類,接口,方法的泛型信息保留到字節碼當中。那麼通過反射,這些信息還是可以獲取到的。但要獲取到具體的泛型類型,一般也只能獲取到繼承父類所使用的泛型類型。
比如:
public class SubClass extends Base<String> { }11
那麼Base所綁定的泛型類型可以被獲取到的。對SubClass.class調用getGenericSuperclass
可以獲取到T所綁定的類型。
Type type = SubClass.class.getGenericSuperclass(); Type targ = ((ParameterizedType) type).getActualTypeArguments()[0]; System.out.println(type); // SubClass<java.lang.String> System.out.println(targ); // class java.lang.String12341234
具體的用法可以參考Gson和Guice的源碼:
https://github.com/google/guice/blob/abc78c361d9018da211690b673accb580a52abf2/core/src/com/google/inject/TypeLiteral.java#L94
https://github.com/google/gson/blob/master/gson/src/main/java/com/google/gson/internal/%24Gson%24Types.java
橋方法
爲了使Java的泛型方法生成的字節碼與1.5以前的字節碼相兼容,由編譯期自己生成的方法。顧名思義,橋方法是一座橋,溝通着泛型與多態。
可以通過Method.isBridge()
方法來判斷一個方法是否是橋接方法,在字節碼中橋接方法會被標記爲ACC_BRIDGE
和ACC_SYNTHETIC
。
public class Fruit<T> { T value; public T getValue() { return value; } } public class Apple extends Fruit<String> { @Override public String getValue() { return "foo was call"; } }123456789101112123456789101112
反編譯生成的字節碼:
public class Apple extends Fruit<java.lang.String> { public Apple(); Code: 0: aload_0 1: invokespecial #1 // Method Fruit."<init>":()V 4: return public java.lang.String getValue(); Code: 0: ldc #2 // String calling 2: areturn public java.lang.Object getValue(); Code: 0: aload_0 1: invokevirtual #3 // Method getValue:()Ljava/lang/String; 4: areturn }123456789101112131415161718123456789101112131415161718
編譯器爲我們自動生成了有一個橋方法,這個橋方法返回類型爲Object,內部調用了我們自定義的另一個getValue方法。
在Java代碼中,方法的特徵簽名只包括方法名稱,參數順序和參數類型,而字節碼中的特徵簽名還包括方法返回值和受查異常表。因此,橋方法public Object getValue()
與public String getValue()
是可以被JVM區分而在同一個Class文件中共存的。
由於編譯期泛型擦除機制,在父類中帶泛型參數的方法會被替換成Object類型。要讓子類重寫父類帶泛型參數的方法,需要通過橋方法直接複寫父類的方法,然後橋方法再調用子類自定義的方法,就以上面作爲例子,子類Apple中的橋方法public Object getValue()
直接override父類Fruit的public Object getValue()
,然後橋方法內部再調用子類Apple的public String getValue()
。因此,Java利用橋方法在保證多態機制不被破壞情況下實現了泛型。