泛型擦除概念
Java的泛型是偽泛型,這是因為Java在編譯期間,所有的泛型信息都會被擦掉,正確理解泛型概念的首要前提是理解類型擦除。Java的泛型基本上都是在編譯器這個層次上實現的,在生成的字節碼中是不包含泛型中的類型信息的,使用泛型的時候加上類型參數,在編譯器編譯的時候會去掉,這個過程成為類型擦除。
例如:List<String>
和 List<Integer>
在編譯后都變成 List
。
證明泛型擦除
ArrayList<String> arrayList1=new ArrayList<String>();
arrayList1.add("abc");
ArrayList<Integer> arrayList2=new ArrayList<Integer>();
arrayList2.add(123);
System.out.println(arrayList1.getClass()==arrayList2.getClass());
我們定義了兩個ArrayList數組,不過一個是ArrayList
ArrayList<Integer> arrayList3=new ArrayList<Integer>();
arrayList3.add(1);//這樣調用add方法只能存儲整形,因為泛型類型的實例為Integer
arrayList3.getClass().getMethod("add", Object.class).invoke(arrayList3, "asd");
for (int i=0;i<arrayList3.size();i++) {
System.out.println(arrayList3.get(i));
}
在程序中定義了一個ArrayList泛型類型實例化為Integer的對象,如果直接調用add方法,那么只能存儲整形的數據。不過當我們利用反射調用add方法的時候,卻可以存儲字符串。這說明了Integer泛型實例在編譯之后被擦除了,只保留了 原始類型。
引用檢查與編譯
說類型變量會在編譯的時候擦除掉,那為什么我們往ArrayList
java編譯器是通過先檢查代碼中泛型的類型,然后再進行類型擦除,在進行編譯的。
ArrayList<String> arrayList1=new ArrayList();
arrayList1.add("1");//編譯通過
arrayList1.add(1);//編譯錯誤
String str1=arrayList1.get(0);//返回類型就是String
ArrayList arrayList2=new ArrayList<String>();
arrayList2.add("1");//編譯通過
arrayList2.add(1);//編譯通過
Object object=arrayList2.get(0);//返回類型就是Object
new ArrayList<String>().add("11");//編譯通過
new ArrayList<String>().add(22);//編譯錯誤
String string=new ArrayList<String>().get(0);//返回類型就是String
類型檢查就是針對引用的,會對這個引用調用的方法進行類型檢測,而無關它真正引用的對象。
泛型擦除與多態的沖突和解決方法
泛型重載變重寫?
public class Pair<T> {
private T value;
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
public class DateInter extends Pair<Date> {
@Override
public void setValue(Date value) {
super.setValue(value);
}
@Override
public Date getValue() {
return super.getValue();
}
}
在這個子類中,我們設定父類的泛型類型為Pair
將父類的泛型類型限定為Date,那么父類里面的兩個方法的參數都為Date類型
public Date getValue() {
return value;
}
public void setValue(Date value) {
this.value = value;
}
實際上,類型擦除后,父類的的泛型類型全部變為了原始類型Object,而子類的類型是Date,參數類型不一樣,這如果實在普通的繼承關系中,根本就不會是重寫,而是重載。
本意是將父類中的泛型轉換為Date類型,子類重寫參數類型為Date的兩個方法
實際上,虛擬機並不能將泛型類型變為Date,只能將類型擦除掉,變為原始類型Object。這樣,我們的本意是進行重寫,實現多態。可是類型擦除后,只能變為了重載。這樣,類型擦除就和多態有了沖突。JVM知道你的本意嗎?知道!!!可是它能直接實現嗎,不能!!!
如果真的不能的話,那我們怎么去重寫我們想要的Date類型參數的方法
JVM采用了一個特殊的方法,來完成這項功能,那就是橋方法。
JVM橋方法
使用bytecode viewer
插件反編譯DateInter
類
public class com/qhong/basic/genericity/DateInter extends com/qhong/basic/genericity/Pair {
// compiled from: DateInter.java
// access flags 0x1
public <init>()V
L0
LINENUMBER 13 L0
ALOAD 0
INVOKESPECIAL com/qhong/basic/genericity/Pair.<init> ()V
RETURN
L1
LOCALVARIABLE this Lcom/qhong/basic/genericity/DateInter; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x1
public setValue(Ljava/util/Date;)V
L0
LINENUMBER 16 L0
ALOAD 0
ALOAD 1
INVOKESPECIAL com/qhong/basic/genericity/Pair.setValue (Ljava/lang/Object;)V
L1
LINENUMBER 17 L1
RETURN
L2
LOCALVARIABLE this Lcom/qhong/basic/genericity/DateInter; L0 L2 0
LOCALVARIABLE value Ljava/util/Date; L0 L2 1
MAXSTACK = 2
MAXLOCALS = 2
// access flags 0x1
public getValue()Ljava/util/Date;
L0
LINENUMBER 20 L0
ALOAD 0
INVOKESPECIAL com/qhong/basic/genericity/Pair.getValue ()Ljava/lang/Object;
CHECKCAST java/util/Date
ARETURN
L1
LOCALVARIABLE this Lcom/qhong/basic/genericity/DateInter; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x1041
public synthetic bridge setValue(Ljava/lang/Object;)V
L0
LINENUMBER 13 L0
ALOAD 0
ALOAD 1
CHECKCAST java/util/Date
INVOKEVIRTUAL com/qhong/basic/genericity/DateInter.setValue (Ljava/util/Date;)V
RETURN
L1
LOCALVARIABLE this Lcom/qhong/basic/genericity/DateInter; L0 L1 0
MAXSTACK = 2
MAXLOCALS = 2
// access flags 0x1041
public synthetic bridge getValue()Ljava/lang/Object;
L0
LINENUMBER 13 L0
ALOAD 0
INVOKEVIRTUAL com/qhong/basic/genericity/DateInter.getValue ()Ljava/util/Date;
ARETURN
L1
LOCALVARIABLE this Lcom/qhong/basic/genericity/DateInter; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
}
我們本意重寫setValue和getValue方法的子類,竟然有4個方法,其實不用驚奇,最后的兩個方法,就是編譯器自己生成的橋方法。可以看到橋方法的參數類型都是Object,也就是說,子類中真正覆蓋父類兩個方法的就是這兩個我們看不到的橋方法。而打在我們自己定義的setvalue和getValue方法上面的@Oveerride只不過是假象。而橋方法的內部實現,就只是去調用我們自己重寫的那兩個方法。
所以,虛擬機巧妙的使用了巧方法,來解決了類型擦除和多態的沖突。
虛擬機兼容方法簽名相同
子類中的橋方法 Object getValue()和Date getValue()是同時存在的,可是如果是常規的兩個方法,他們的方法簽名是一樣的,也就是說虛擬機根本不能分別這兩個方法。如果是我們自己編寫Java代碼,這樣的代碼是無法通過編譯器的檢查的,但是虛擬機卻是允許這樣做的,因為虛擬機通過參數類型和返回類型來確定一個方法,所以編譯器為了實現泛型的多態允許自己做這個看起來“不合法”的事情,然后交給虛擬器去區別
<T> VS <?>
不同點:
<T>
用於 泛型的定義,例如class MyGeneric<T> {...}
<?>
用於 泛型的聲明,即泛型的使用,例如MyGeneric<?> g = new MyGeneric<>();
相同點:
都可以指定上界和下界,例如:
class MyGeneric<T extends Collection> {...}
class MyGeneric<T super List> {...}
MyGeneric<? extends Collection> g = new MyGeneric<>();`
`MyGeneric<? super List> g = new MyGeneric<>();
靜態方法,靜態類中的泛型
泛型類中的靜態方法和靜態變量不可以使用泛型類所聲明的泛型類型參數
public class Test2<T> {
public static T one; //編譯錯誤
public static T show(T one){ //編譯錯誤
return null;
}
}
因為泛型類中的泛型參數的實例化是在定義對象的時候指定的,而靜態變量和靜態方法不需要使用對象來調用。對象都沒有創建,如何確定這個泛型參數是何種類型,所以當然是錯誤的。
靜態方法自己定義的泛型
public class Test2<T> {
public static <T >T show(T one){//這是正確的
return null;
}
}
因為這是一個泛型方法,在泛型方法中使用的T是自己在方法中定義的T,而不是泛型類中的T。