類型擦除
- 編譯器在編譯期間所以的泛型信息都會被擦除
編譯 .java 生成的 .class (字節碼文件)中數據類型必須是確定好的。
如果一個 class 是泛型類,或者含有泛型方法,那麼編譯器在編譯時會將其中的類型變量去掉,生產一個與泛型類同名的原始類。在 原始類class文件 中的是其真正的類型(原始類型)。
注:
<>所修飾的部分(例:< T >)直接被擦除,而之後的用到的類型變量( T )會被原始類型代替。
原始類型:類型限界(無類型限界爲Object)
定義泛型類Generic1和Generic2
class Generic1<T> {
T value;
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
class Generic2<T extends A> { // A爲類型限界
T value;
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
// 自定義的類
class A {
String aName = "A";
}
class B extends A {
String bName = "B";
}
Generic1和Generic2類分別經過類型擦除後生產的(原始類)字節碼如下
/*
* 原始類型爲:Object
*/
class Generic1{
Object value;
public Object getValue() {
return value;
}
public void setValue(Object value) {
this.value = value;
}
}
/*
* 原始類型爲:A
*/
class Generic2 {
A value;
public A getValue() {
return value;
}
public void setValue(A value) {
this.value = value;
}
}
總結:
如果泛型類型的類型變量沒有限定() ,用Object作爲原始類型;
如果有限定(),用A作爲原始類型;
如果有多個限定(<T extends A1 & A2>),用第一個邊界的類型變量A1類作爲原始類型;
類型轉換
- 當一個具有擦除返回類型的泛型方法被調用時會進行強制類型轉換
Generic1中的
public T getValue() {
return value;
}
調用getValue方法時
Generic1<String> g1 = new Generic1<>();
g1.setValue("Hello world!");
String s1 = g1.getValue();// 這裏編譯器編譯時處理成:String s1 = (String) g1.getValue()
由於原始類型是Object,返回的value的類型是Object,但是在調用這個方法的地方會根據類型變量進行強轉(做了一個checkcast()操作,即檢查< String >中的類型並強轉)
ArrayList中的
E elementData(int index) {
return (E) elementData[index];
}
public E get(int index) {
rangeCheck(index);
return elementData(index);// 調用elementData方法
}
調用get方法時
ArrayList<Date> list = new ArrayList<Date>();
list.add(new Date());
Date myDate = list.get(0);
由於原始類型是Object,方法返回值是Object,但是在調用時會進行強制類型轉換。
以上需要注意的是:不是在方法裏強轉,是在調用的地方強轉。
橋方法
- 爲了避免類型變量擦除所帶來的多態問題
定義一個父類
class Generic<T> {
T value;
public void setValue(T value) {
this.value = value;
}
public T getValue() {
return value;
}
}
定義一個子類,重寫父類方法
class MyGeneric extends Generic<String> {
@Override
public void setValue(String value) {
super.setValue(value);
}
@Override
public String getValue() {
return super.getValue();
}
}
子類重寫父類的方法,但是
// 繼承父類的已經類型擦除後的方法
public void setValue(Object value){...}
public T getValue(){...}
// 子類新增(重寫)的方法
public void setValue(String value){...}
public String getValue(){...}
由於參數類型不同這並不能算是重寫,爲了達到重寫的目的,編譯器使用橋方法解決。
編譯器在 MyGeneric 類中生成了兩個橋方法(這兩個橋方法會調用子類新增的方法)
public void setValue(Object value) {
setValue((String) value);// 子類新增的setValue(String value)方法
}
public Object getValue() {
return getValue();// 子類新增的getValue()方法(返回的 String 類型的 getValue 方法)
}
也就是說在重寫方法後的子類中通過編譯器會變成如下情形
class MyGeneric extends Generic<String> {
public void setValue(String value){...} // 自己定義(重寫)的方法
public void setValue(Object value){...} // 編譯器生成的橋方法
public String getValue(){...} // 自己定義(重寫)的方法
public Object getValue(){...} // 編譯器生成的橋方法
}
值得注意的是getValue方法:
編譯器允許在同一個類中出現方法簽名相同的多個方法嗎?
方法簽名(方法名+參數列表)用來確定一個方法;
人爲是不能在同一個類中編寫出方法簽名一樣的多個方法,否則編譯器會報錯;
但是,編譯器自己能夠創建出方法簽名一樣而返回類型不同的方法,JVM會用參數類型和返回類型來確定一個方法。