Java泛型
Java泛型(generics)是JDK 5中引入的一個新特性,允許在定義類和介面的時候使用型別引數(type parameter)。宣告的型別引數在使用時用具體的型別來替換。泛型最主要的應用是在JDK 5中的新集合類框架中。泛型的引入可以解決JDK5之前的集合類框架在使用過程中較為容出現的執行時型別轉換異常,因為編譯器可以在編譯時通過型別檢查,規避掉一些潛在的風險。
在JDK5之前,使用集合框架時,是沒有型別資訊的,統一使用Object,我找了一段JDK4 List介面的方法簽名
如下是JDK5開始引入泛型,List介面的改動,新的方法簽名,引入了型別引數。boolean add(E e);
複製程式碼
在JDK5之前,使用集合類時,可以往其中新增任意元素,因為其中的型別是Object,在取出的階段做強制轉換,由此可能引發很多意向不到的執行時強制轉換錯誤,比如以下程式碼。
public class Test1 {
public static void main(String[] args) {
List a = new ArrayList();
a.add("123");
a.add(1);
// 以上程式碼可以正常通過編譯,其中同時含有了Integer型別和String型別
for (int i = 0 ; i < a.size(); i++) {
int result = (Integer)a.get(i); // 在取出時需要對Object進行強制轉型
System.out.println(result);
}
}
}
複製程式碼
如上程式碼就會在執行時階段帶來強轉異常,在編譯時間不能夠排查出潛在風險。
如果使用泛型機制,可以在編譯期間就檢查出List的型別插入的有問題,進行規避,如下程式碼。public class Test1 {
public static void main(String[] args) {
List<Integer> a = new ArrayList();
a.add("123"); // 編譯不通過
a.add(1);
}
}
複製程式碼
引入泛型後,編譯器會在編譯時先根據型別引數進行型別檢查,杜絕掉一些潛在風險。 為何說是在編譯時檢查,因為在執行時仍然是可以通過反射,將不符合型別引數的資料插入至list中,如下程式碼所示。
public class Test1 {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
List<Integer> a = new ArrayList();
List b = new ArrayList();
a.getClass().getMethod("add",Object.class).invoke(a,"abc");
// 以上程式碼編譯通過,執行通過
}
}
複製程式碼
引入泛型的同時,也為了相容JDK5之前的類庫,JDK5開始引入的其實是偽泛型,在生成的Java位元組碼中是不包含泛型中的型別資訊的。使用泛型的時候加上的型別引數,會在編譯器在編譯的時候去掉。這個過程就稱為型別擦除。如在程式碼中定義的List等型別,在編譯後都會變成List,也就自然相容了JDK5之前的程式碼。 Java的泛型機制和C++等的泛型機制實現不同,Java的泛型靠的還是型別擦除,目的碼只會生成一份,犧牲的是執行速度。C++的模板會對針對不同的模板引數靜態例項化,目的碼體積會稍大一些,執行速度會快很多。
進行型別擦除後,型別引數原始型別(raw type)就是擦除去了泛型資訊,最後在位元組碼中的型別變數的真正型別。無論何時定義一個泛型型別,相應的原始型別都會被自動地提供。型別變數被擦除,並使用其限定型別(無限定的變數用Object)替換。
class Pair<T> {
private T value;
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
Pair<T>的原始型別為:
class Pair {
private Object value;
public Object getValue() {
return value;
}
public void setValue(Object value) {
this.value = value;
}
}
複製程式碼
在Pair中,型別擦除,使用Object,其結果就是一個普通的類,如同泛型加入java程式語言之前已經實現的那樣。在程式中可以包含不同型別的Pair,如Pair或Pair,但是,擦除型別後它們就成為原始的Pair型別了,原始型別都是Object。ArrayList被擦除型別後,原始型別也變成了Object,通過反射我們就可以儲存字串了。
在呼叫泛型方法的時候,可以指定泛型,也可以不指定泛型。在不指定泛型的情況下,泛型變數的型別為 該方法中的幾種型別的同一個父類的最小級,直到Object。在指定泛型的時候,該方法中的幾種型別必須是該泛型例項型別或者其子類。
public class Test1 {
public static void main(String[] args) {
/** 不指定泛型的時候 */
int i = Test1.add(1, 2); // 這兩個引數都是Integer,所以T為Integer型別
Number f = Test1.add(1, 1.2);// 這兩個引數一個是Integer,以風格是Float,所以取同一父類的最小級,為Number
Object o = Test1.add(1, "asd");// 這兩個引數一個是Integer,以風格是Float,所以取同一父類的最小級,為Object
/** 指定泛型的時候 */
int a = Test1.<Integer> add(1, 2);// 指定了Integer,所以只能為Integer型別或者其子類
int b = Test1.<Integer> add(1, 2.2);// 編譯錯誤,指定了Integer,不能為Float
Number c = Test1.<Number> add(1, 2.2); // 指定為Number,所以可以為Integer和Float
}
// 這是一個簡單的泛型方法
public static <T> T add(T x, T y) {
return y;
}
}
複製程式碼
因為型別擦除的問題,所有的泛型型別變數最後都會被替換為原始型別,但在泛型的使用中,我們不需要對取出的資料做強制轉換。
public class Test1 {
public static void main(String[] args) {
List<Integer> a = new ArrayList();
a.add(1);
for (int i = 0 ; i < a.size(); i++) {
int result = a.get(i);
System.out.println(result);
}
}
}
複製程式碼
我們從位元組碼的角度來探索一下。
public static void main(java.lang.String[]);
Code:
0: new #2 // class java/util/ArrayList
3: dup
4: invokespecial #3 // Method java/util/ArrayList."<init>":()V
7: astore_1
8: aload_1
9: iconst_1
10: invokestatic #4 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
13: invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
18: pop
19: iconst_0
20: istore_2
21: iload_2
22: aload_1
23: invokeinterface #6, 1 // InterfaceMethod java/util/List.size:()I
28: if_icmpge 58
31: aload_1
32: iload_2
33: invokeinterface #7, 2 // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
38: checkcast #8 // class java/lang/Integer 這裡JVM做了強轉
41: invokevirtual #9 // Method java/lang/Integer.intValue:()I
44: istore_3
45: getstatic #10 // Field java/lang/System.out:Ljava/io/PrintStream;
48: iload_3
49: invokevirtual #11 // Method java/io/PrintStream.println:(I)V
52: iinc 2, 1
55: goto 21
58: return
複製程式碼
在偏移量38的位置可以看到,JVM使用了checkcast指令,說明雖然在編譯時進行了型別擦除,但是JVM中仍然保留了型別引數的元資訊,在取出時自動進行了強轉,這也算是使用泛型的方便之處吧。
在別人的例子有看到說型別擦除和多型的衝突,舉了一個例子。
public class Test1 {
public static void main(String[] args) {
DateInter dateInter = new DateInter();
dateInter.setValue(new Date());
dateInter.setValue(new Object());// 編譯錯誤
}
}
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 Date getValue() {
return super.getValue();
}
@Override
public void setValue(Date value) {
super.setValue(value);
}
}
複製程式碼
因為在型別擦除後,父類也就變成了一個普通的類,如下所示
class Pair {
private Object value;
public Object getValue() {
return value;
}
public void setValue(Object value) {
this.value = value;
}
}
複製程式碼
但這樣setValue就從重寫變成了過載,顯然打破了想達到的目的,那麼JVM是如何幫助解決這個衝突的呢?答案是 JVM幫我們搭了一個橋,具體我們從位元組碼的角度再來看看。
class DateInter extends Pair<java.util.Date> {
DateInter();
Code:
0: aload_0
1: invokespecial #1 // Method Pair."<init>":()V
4: return
public java.util.Date getValue();
Code:
0: aload_0
1: invokespecial #2 // Method Pair.getValue:()Ljava/lang/Object;
4: checkcast #3 // class java/util/Date
7: areturn
public void setValue(java.util.Date);
Code:
0: aload_0
1: aload_1
2: invokespecial #4 // Method Pair.setValue:(Ljava/lang/Object;)V
5: return
public void setValue(java.lang.Object);
Code:
0: aload_0
1: aload_1
2: checkcast #3 // class java/util/Date
5: invokevirtual #5 // Method setValue:(Ljava/util/Date;)V
8: return
public java.lang.Object getValue();
Code:
0: aload_0
1: invokevirtual #6 // Method getValue:()Ljava/util/Date;
4: areturn
}
複製程式碼
從編譯的結果來看,我們本意重寫setValue和getValue方法的子類,有4個方法,最後的兩個方法,就是編譯器自己生成的橋接方法。可以看到橋方法的引數型別都是Object,也就是說,子類中真正覆蓋父類兩個方法的就是這兩個我們看不到的橋方法,打在我們自己定義的setvalue和getValue方法上面的@Oveerride只不過是假象。而橋方法的內部實現,就只是去呼叫我們自己重寫的那兩個方法。 所以,虛擬機器巧妙的使用了巧方法,來解決了型別擦除和多型的衝突。
最後附上最近在瀏覽一些別人經驗時得到一些tips。
- 使用JSON串反序列化物件集合時,記得標註物件的class型別,不然會得到一個只有原始型別也就是Object的集合,可能引起型別轉換錯誤,尤其是在服務呼叫的這種場景下。
- 重視編譯器提出的警告資訊。