天天看點

Java泛型類型擦除以及類型擦除帶來的問題

一、Java泛型的實作方法:類型擦除

大家都知道,Java的泛型是僞泛型,這是因為Java在編譯期間,所有的泛型資訊都會被擦掉,正确了解泛型概念的首要前提是了解類型擦除。Java的泛型基本上都是在編譯器這個層次上實作的,在生成的位元組碼中是不包含泛型中的類型資訊的,使用泛型的時候加上類型參數,在編譯器編譯的時候會去掉,這個過程成為類型擦除。

如在代碼中定義​

​List<Object>​

​和​

​List<String>​

​等類型,在編譯後都會變成​

​List​

​,JVM看到的隻是​

​List​

​,而由泛型附加的類型資訊對JVM是看不到的。Java編譯器會在編譯時盡可能的發現可能出錯的地方,但是仍然無法在運作時刻出現的類型轉換異常的情況,類型擦除也是 Java 的泛型與 C++ 模闆機制實作方式之間的重要差別。

通過兩個例子證明Java類型的類型擦除

1、原始類型相等

public class Test {

public static void main(String[] args) {

ArrayList<String> list1 = new ArrayList<String>();
list1.add("abc");

ArrayList<Integer> list2 = new ArrayList<Integer>();
list2.add(123);

System.out.println(list1.getClass() == list2.getClass());
    }

}
      

在這個例子中,我們定義了兩個​

​ArrayList​

​數組,不過一個是​

​ArrayList<String>​

​泛型類型的,隻能存儲字元串;一個是​

​ArrayList<Integer>​

​泛型類型的,隻能存儲整數,最後,我們通過​

​list1​

​對象和​

​list2​

​對象的​

​getClass()​

​方法擷取他們的類的資訊,最後發現結果為​

​true​

​。說明泛型類型​

​String​

​Integer​

​都被擦除掉了,隻剩下原始類型。

2、通過反射添加其它類型元素

public class Test {

public static void main(String[] args) throws Exception {

ArrayList<Integer> list = new ArrayList<Integer>();

list.add(1);  //這樣調用 add 方法隻能存儲整形,因為泛型類型的執行個體為 Integer

list.getClass().getMethod("add", Object.class).invoke(list, "asd");

for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));//輸出:1 asd
        }
    }
}
      

在程式中定義了一個​

​ArrayList​

​泛型類型執行個體化為​

​Integer​

​對象,如果直接調用​

​add()​

​方法,那麼隻能存儲整數資料,不過當我們利用反射調用​

​add()​

​方法的時候,卻可以存儲字元串,這說明了​

​Integer​

​泛型執行個體在編譯之後被擦除掉了,隻保留了原始類型。

二、類型擦除後保留的原始類型

在上面,兩次提到了原始類型,什麼是原始類型?

原始類型 就是擦除去了泛型資訊,最後在位元組碼中的類型變量的真正類型,無論何時定義一個泛型,相應的原始類型都會被自動提供,類型變量擦除,并使用其限定類型(無限定的變量用Object)替換。

1、原始類型Object

public class Pair<T> {  
private T value;  
public T getValue() {  
return value;  
    }  
public void setValue(T  value) {  
this.value = value;  
    }  
}  
      

Pair的原始類型為:

public class Pair {  
private Object value;  
public Object getValue() {  
return value;  
    }  
public void setValue(Object  value) {  
this.value = value;  
    }  
}
      

因為在​

​Pair<T>​

​中,T 是一個無限定的類型變量,是以用​

​Object​

​替換,其結果就是一個普通的類,如同泛型加入Java語言之前的已經實作的樣子。在程式中可以包含不同類型的​

​Pair​

​,如​

​Pair<String>​

​或​

​Pair<Integer>​

​,但是擦除類型後他們的就成為原始的​

​Pair​

​類型了,原始類型都是​

​Object​

​。

從上面的"一、2"中,我們也可以明白​

​ArrayList<Integer>​

​被擦除類型後,原始類型也變為​

​Object​

​,是以通過反射我們就可以存儲字元串了。

如果類型變量有限定,那麼原始類型就用第一個邊界的類型變量類替換。

比如: Pair這樣聲明的話

public class Pair<T extends Comparable> {}
      

那麼原始類型就是​

​Comparable​

要區分原始類型和泛型變量的類型。

在調用泛型方法時,可以指定泛型,也可以不指定泛型。

  • 在不指定泛型的情況下,泛型變量的類型為該方法中的幾種類型的同一父類的最小級,直到 Object。
  • 在指定泛型的情況下,該方法的幾種類型必須是該泛型的執行個體的類型或者其子類。
public class Test {  
public static void main(String[] args) {  

/**不指定泛型的時候*/  
int i = Test.add(1, 2); //這兩個參數都是Integer,是以T為Integer類型  
Number f = Test.add(1, 1.2); //這兩個參數一個是Integer,以風格是Float,是以取同一父類的最小級,為Number  
Object o = Test.add(1, "asd"); //這兩個參數一個是Integer,以風格是Float,是以取同一父類的最小級,為Object  

/**指定泛型的時候*/  
int a = Test.<Integer>add(1, 2); //指定了Integer,是以隻能為Integer類型或者其子類  
int b = Test.<Integer>add(1, 2.2); //編譯錯誤,指定了Integer,不能為Float  
Number c = Test.<Number>add(1, 2.2); //指定為Number,是以可以為Integer和Float  
    }  

//這是一個簡單的泛型方法  
public static <T> T add(T x,T y){  
return y;  
    }  
}
      

其實在泛型類中,不指定泛型的時候,也差不多,隻不過這個時候的泛型為​

​Object​

​,就比如​

​ArrayList​

​中,如果不指定泛型,那麼這個​

​ArrayList​

​可以存儲任意的對象。

2、Object泛型

public static void main(String[] args) {  
ArrayList list = new ArrayList();  
list.add(1);  
list.add("121");  
list.add(new Date());  
}  
      

三、類型擦除引起的問題及解決方法

因為種種原因,Java不能實作真正的泛型,隻能使用類型擦除來實作僞泛型,這樣雖然不會有類型膨脹問題,但是也引起來許多新問題,是以,SUN對這些問題做出了種種限制,避免我們發生各種錯誤。

1、先檢查再編譯以及編譯的對象和引用傳遞問題

Q: 既然說類型變量會在編譯的時候擦除掉,那為什麼我們往 ArrayList 建立的對象中添加整數會報錯呢?不是說泛型變量String會在編譯的時候變為Object類型嗎?為什麼不能存别的類型呢?既然類型擦除了,如何保證我們隻能使用泛型變量限定的類型呢?

A: Java編譯器是通過先檢查代碼中泛型的類型,然後在進行類型擦除,再進行編譯。

例如:

public static  void main(String[] args) {  

ArrayList<String> list = new ArrayList<String>();  
list.add("123");  
list.add(123);//編譯錯誤  
}
      

在上面的程式中,使用​

​add​

​方法添加一個整型,在IDE中,直接會報錯,說明這就是在編譯之前的檢查,因為如果是在編譯之後檢查,類型擦除後,原始類型為​

​Object​

​,是應該允許任意引用類型添加的。可實際上卻不是這樣的,這恰恰說明了關于泛型變量的使用,是會在編譯之前檢查的。

那麼,這個類型檢查是針對誰的呢?我們先看看參數化類型和原始類型的相容。

以 ArrayList舉例子,以前的寫法:

ArrayList list = new ArrayList();  
      

現在的寫法:

ArrayList<String> list = new ArrayList<String>();
      

如果是與以前的代碼相容,各種引用傳值之間,必然會出現如下的情況:

ArrayList<String> list1 = new ArrayList(); //第一種 情況
ArrayList list2 = new ArrayList<String>(); //第二種 情況
      

這樣是沒有錯誤的,不過會有個編譯時警告。

不過在第一種情況,可以實作與完全使用泛型參數一樣的效果,第二種則沒有效果。

因為類型檢查就是編譯時完成的,​

​new ArrayList()​

​隻是在記憶體中開辟了一個存儲空間,可以存儲任何類型對象,而真正設計類型檢查的是它的引用,因為我們是使用它引用​

​list1​

​來調用它的方法,比如說調用​

​add​

​方法,是以​

​list1​

​引用能完成泛型類型的檢查。而引用​

​list2​

​沒有使用泛型,是以不行。

舉例子:

public class Test {  

public static void main(String[] args) {  

ArrayList<String> list1 = new ArrayList();  
list1.add("1"); //編譯通過  
list1.add(1); //編譯錯誤  
String str1 = list1.get(0); //傳回類型就是String  

ArrayList list2 = new ArrayList<String>();  
list2.add("1"); //編譯通過  
list2.add(1); //編譯通過  
Object object = list2.get(0); //傳回類型就是Object  

new ArrayList<String>().add("11"); //編譯通過  
new ArrayList<String>().add(22); //編譯錯誤  

String str2 = new ArrayList<String>().get(0); //傳回類型就是String  
    }  

}  
      

通過上面的例子,我們可以明白,類型檢查就是針對引用的,誰是一個引用,用這個引用調用泛型方法,就會對這個引用調用的方法進行類型檢測,而無關它真正引用的對象。

泛型中參數話類型為什麼不考慮繼承關系?

在Java中,像下面形式的引用傳遞是不允許的:

ArrayList<String> list1 = new ArrayList<Object>(); //編譯錯誤  
ArrayList<Object> list2 = new ArrayList<String>(); //編譯錯誤
      

我們先看第一種情況,将第一種情況拓展成下面的形式:

ArrayList<Object> list1 = new ArrayList<Object>();  
list1.add(new Object());  
list1.add(new Object());  
ArrayList<String> list2 = list1; //編譯錯誤
      

實際上,在第4行代碼的時候,就會有編譯錯誤。那麼,我們先假設它編譯沒錯。那麼當我們使用​

​list2​

​引用用​

​get()​

​方法取值的時候,傳回的都是​

​String​

​類型的對象(上面提到了,類型檢測是根據引用來決定的),可是它裡面實際上已經被我們存放了​

​Object​

​類型的對象,這樣就會有​

​ClassCastException​

​了。是以為了避免這種極易出現的錯誤,Java不允許進行這樣的引用傳遞。(這也是泛型出現的原因,就是為了解決類型轉換的問題,我們不能違背它的初衷)。

再看第二種情況,将第二種情況拓展成下面的形式:

ArrayList<String> list1 = new ArrayList<String>();  
list1.add(new String());  
list1.add(new String());

ArrayList<Object> list2 = list1; //編譯錯誤
      

沒錯,這樣的情況比第一種情況好的多,最起碼,在我們用​

​list2​

​取值的時候不會出現​

​ClassCastException​

​,因為是從​

​String​

​轉換為​

​Object​

​。可是,這樣做有什麼意義呢,泛型出現的原因,就是為了解決類型轉換的問題。我們使用了泛型,到頭來,還是要自己強轉,違背了泛型設計的初衷。是以java不允許這麼幹。再說,你如果又用​

​list2​

​往裡面​

​add()​

​新的對象,那麼到時候取得時候,我怎麼知道我取出來的到底是​

​String​

​類型的,還是​

​Object​

​類型的呢?

是以,要格外注意,泛型中的引用傳遞的問題。

2、自動類型轉換

因為類型擦除的問題,是以所有的泛型類型變量最後都會被替換為原始類型。

既然都被替換為原始類型,那麼為什麼我們在擷取的時候,不需要進行強制類型轉換呢?

看下​

​ArrayList.get()​

​方法:

public E get(int index) {  

RangeCheck(index);  

return (E) elementData[index];  

}
      

可以看到,在​

​return​

​之前,會根據泛型變量進行強轉。假設泛型類型變量為​

​Date​

​,雖然泛型資訊會被擦除掉,但是會将​

​(E) elementData[index]​

​,編譯為​

​(Date) elementData[index]​

​。是以我們不用自己進行強轉。當存取一個泛型域時也會自動插入強制類型轉換。假設​

​Pair​

​類的​

​value​

​域是​

​public​

​的,那麼表達式:

Date date = pair.value;
      

也會自動地在結果位元組碼中插入強制類型轉換。

3、類型擦除與多态的沖突和解決方法

現在有這樣一個泛型類:

class Pair<T> {  

private T value;  

public T getValue() {  
return value;  
    }  

public void setValue(T value) {  
this.value = value;  
    }  
}
      

然後我們想要一個子類繼承它。

class DateInter extends Pair<Date> {  

@Override
public void setValue(Date value) {  
super.setValue(value);  
    }  

@Override
public Date getValue() {  
return super.getValue();  
    }  
}
      

在這個子類中,我們設定父類的泛型類型為​

​Pair<Date>​

​,在子類中,我們覆寫了父類的兩個方法,我們的原意是這樣的:将父類的泛型類型限定為​

​Date​

​,那麼父類裡面的兩個方法的參數都為​

​Date​

​類型。

public Date getValue() {  
return value;  
}  

public void setValue(Date value) {  
this.value = value;  
}
      

是以,我們在子類中重寫這兩個方法一點問題也沒有,實際上,從他們的​

​@Override​

​标簽中也可以看到,一點問題也沒有,實際上是這樣的嗎?

分析:實際上,類型擦除後,父類的的泛型類型全部變為了原始類型​

​Object​

​,是以父類編譯之後會變成下面的樣子:

class Pair {  
private Object value;  

public Object getValue() {  
return value;  
    }  

public void setValue(Object  value) {  
this.value = value;  
    }  
}  
      

再看子類的兩個重寫的方法的類型:

@Override  
public void setValue(Date value) {  
super.setValue(value);  
}  
@Override  
public Date getValue() {  
return super.getValue();  
}
      

先來分析​

​setValue​

​方法,父類的類型是​

​Object​

​,而子類的類型是​

​Date​

​,參數類型不一樣,這如果實在普通的繼承關系中,根本就不會是重寫,而是重載。

我們在一個main方法測試一下:

public static void main(String[] args) throws ClassNotFoundException {  
DateInter dateInter = new DateInter();  
dateInter.setValue(new Date());                  
dateInter.setValue(new Object()); //編譯錯誤  
}
      

如果是重載,那麼子類中兩個​

​setValue​

​方法,一個是參數​

​Object​

​類型,一個是​

​Date​

​類型,可是我們發現,根本就沒有這樣的一個子類繼承自父類的Object類型參數的方法。是以說,卻是是重寫了,而不是重載了。

為什麼會這樣呢?

原因是這樣的,我們傳入父類的泛型類型是​

​Date,Pair<Date>​

​,我們的本意是将泛型類變為如下:

class Pair {  
private Date value;  
public Date getValue() {  
return value;  
    }  
public void setValue(Date value) {  
this.value = value;  
    }  
}
      

然後再子類中重寫參數類型為Date的那兩個方法,實作繼承中的多态。

可是由于種種原因,虛拟機并不能将泛型類型變為​

​Date​

​,隻能将類型擦除掉,變為原始類型​

​Object​

​。這樣,我們的本意是進行重寫,實作多态。可是類型擦除後,隻能變為了重載。這樣,類型擦除就和多态有了沖突。JVM知道你的本意嗎?知道!!!可是它能直接實作嗎,不能!!!如果真的不能的話,那我們怎麼去重寫我們想要的​

​Date​

​類型參數的方法啊。

于是JVM采用了一個特殊的方法,來完成這項功能,那就是橋方法。

首先,我們用​

​javap -c className​

​的方式反編譯下​

​DateInter​

​子類的位元組碼,結果如下:

class com.tao.test.DateInter extends com.tao.test.Pair<java.util.Date> {  
  com.tao.test.DateInter();  
    Code:  
       0: aload_0  
       1: invokespecial #8                  // Method com/tao/test/Pair."<init>":()V  
       4: return  

  public void setValue(java.util.Date);  //我們重寫的setValue方法  
    Code:  
       0: aload_0  
       1: aload_1  
       2: invokespecial #16                 // Method com/tao/test/Pair.setValue:(Ljava/lang/Object;)V  
       5: return  

  public java.util.Date getValue();    //我們重寫的getValue方法  
    Code:  
       0: aload_0  
       1: invokespecial #23                 // Method com/tao/test/Pair.getValue:()Ljava/lang/Object;  
       4: checkcast     #26                 // class java/util/Date  
       7: areturn  

  public java.lang.Object getValue();     //編譯時由編譯器生成的橋方法  
    Code:  
       0: aload_0  
       1: invokevirtual #28                 // Method getValue:()Ljava/util/Date 去調用我們重寫的getValue方法;  
       4: areturn  

  public void setValue(java.lang.Object);   //編譯時由編譯器生成的橋方法  
    Code:  
       0: aload_0  
       1: aload_1  
       2: checkcast     #26                 // class java/util/Date  
       5: invokevirtual #30                 // Method setValue:(Ljava/util/Date; 去調用我們重寫的setValue方法)V  
       8: return  
}
      

從編譯的結果來看,我們本意重寫​

​setValue​

​getValue​

​方法的子類,竟然有4個方法,其實不用驚奇,最後的兩個方法,就是編譯器自己生成的橋方法。可以看到橋方法的參數類型都是Object,也就是說,子類中真正覆寫父類兩個方法的就是這兩個我們看不到的橋方法。而在我們自己定義的​

​setvalue​

​getValue​

​方法上面的​

​@Oveerride​

​隻不過是假象。而橋方法的内部實作,就隻是去調用我們自己重寫的那兩個方法。

是以,虛拟機巧妙的使用了橋方法,來解決了類型擦除和多态的沖突。

不過,要提到一點,這裡面的​

​setValue​

​getValue​

​這兩個橋方法的意義又有不同。

​setValue​

​方法是為了解決類型擦除與多态之間的沖突。

而​

​getValue​

​卻有普遍的意義,怎麼說呢,如果這是一個普通的繼承關系:

那麼父類的​

​getValue​

​方法如下:

public Object getValue() {  
return value;  
}
      

而子類重寫的方法是:

public Date getValue() {  
return super.getValue();  
}
      

其實這在普通的類繼承中也是普遍存在的重寫,這就是協變。

關于協變:。。。。。。

并且,還有一點也許會有疑問,子類中的橋方法​

​Object getValue()​

​Date getValue()​

​是同時存在的,可是如果是正常的兩個方法,他們的方法簽名是一樣的,也就是說虛拟機根本不能分别這兩個方法。如果是我們自己編寫Java代碼,這樣的代碼是無法通過編譯器的檢查的,但是虛拟機卻是允許這樣做的,因為虛拟機通過參數類型和傳回類型來确定一個方法,是以編譯器為了實作泛型的多态允許自己做這個看起來“不合法”的事情,然後交給虛拟器去差別。

4、泛型類型變量不能是基本資料類型

不能用類型參數替換基本類型。就比如,沒有​

​ArrayList<double>​

​,隻有​

​ArrayList<Double>​

​。因為當類型擦除後,​

​ArrayList​

​的原始類型變為​

​Object​

​,但是​

​Object​

​類型不能存儲​

​double​

​值,隻能引用​

​Double​

​的值。

5、編譯時集合的instanceof

ArrayList<String> arrayList = new ArrayList<String>();
      

因為類型擦除之後,​

​ArrayList<String>​

​隻剩下原始類型,泛型資訊​

​String​

​不存在了。

那麼,編譯時進行類型查詢的時候使用下面的方法是錯誤的

if( arrayList instanceof ArrayList<String>)
      

6、泛型在靜态方法和靜态類中的問題

泛型類中的靜态方法和靜态變量不可以使用泛型類所聲明的泛型類型參數

舉例說明:

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。