我的部落格 轉載請註明原創出處。
序
之所以會想來寫泛型相關的內容,是因為看到這樣的一段程式碼:
當時我的內心是這樣的:
所以就趕緊去複習了下,記錄下來。基礎不紮實,原始碼看不懂啊。
泛型介紹
Java 泛型(generics)是 JDK 5 中引入的一個新特性,泛型提供了編譯時型別安全檢測機制,該機制允許程式設計師在編譯時檢測到非法的型別。泛型的本質是引數化型別,也就是說所操作的資料型別被指定為一個引數,在Java集合框架裡使用的非常廣泛。
定義的重點是提供了編譯時型別安全檢測機制。比如有這樣的一個泛型類:
public class Generics <T> {
private T value;
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
然後寫這樣一個類:
public class Generics {
private Object value;
public Object getValue() {
return value;
}
public void setValue(Object value) {
this.value = value;
}
}
它們同樣都能儲存所有值,但是泛型類有編譯時型別安全檢測機制:
泛型類
一個類定義了一個或多個型別變數,那麼就是泛型類。語法是在類名後用尖括號括起來,型別變數寫在裡面用逗號分開。然後就可以在方法的返回型別、引數和域、區域性變數中使用型別變數了,但是不能在有static
修飾符修飾的方法或域中使用。例子:
類定義參考上文例子
使用形式
Generics<String> generics = new Generics<String>();
後面尖括號內的內容在Jdk7以後可以省略
Generics<String> generics = new Generics<>();
泛型方法
一個方法定義了一個或多個型別變數,那麼就是泛型方法。語法是在方法修飾符後面、返回型別前面用尖括號括起來,型別變數寫在裡面用逗號分開。泛型方法可以定義在普通類和泛型類中,泛型方法可以被static
修飾符修飾。
例子:
private <U> void out(U u) {
System.out.println(u);
}
呼叫形式,
Test.<String>out("test");
大部分情況下<String>都可以省略,編譯器可以推斷出來型別
Test.out("test");
型別變數的限定
有時候我們會有希望限定型別變數的情況,比如限定指定的型別變數需要實現List
介面,這樣我們就可以在程式碼對型別變數呼叫List
介面裡的方法,而不用擔心會沒有這個方法。
public class Generics <T extends List> {
private T value;
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
public void add(Object u) {
value.add(u);
}
public static void main(String[] args) {
Generics<List> generics = new Generics<>();
generics.setValue(new ArrayList<>());
generics.add("ss");
System.out.println(generics.getValue());
}
}
限定的語法是在型別變數的後面加extends
關鍵字,然後加限定的型別,多個限定的型別要用&
分隔。型別變數和限定的型別可以是類也可以是介面,因為Java中類只能繼承一個類,所以限定的型別是類的話一定要在限定列表的第一個。
型別擦除
型別擦除是為了相容而搞出來的,大意就是在虛擬機器裡是沒有泛型型別,泛型只存在於編譯期間。泛型型別變數會在編譯後被擦除,用第一個限定型別替換(沒有限定型別的用Object
替換)。上文中的Generics <T>
泛型類被擦除後會產生對應的一個原始型別:
public class Generics {
private Object value;
public Object getValue() {
return value;
}
public void setValue(Object value) {
this.value = value;
}
}
之所以我們能設定和返回正確的型別是因為編譯器自動插入了型別轉換的指令。
public static void main(String[] args) {
Generics<String> generics = new Generics<>();
generics.setValue("ss");
System.out.println(generics.getValue());
}
javac Generics.java
javap -c Generics
編譯後的程式碼
public static void main(java.lang.String[]);
Code:
0: new #3 // class generics/Generics
3: dup
4: invokespecial #4 // Method "<init>":()V
7: astore_1
8: aload_1
9: ldc #5 // String ss
11: invokevirtual #6 // Method setValue:(Ljava/lang/Object;)V
14: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
17: aload_1
18: invokevirtual #8 // Method getValue:()Ljava/lang/Object;
21: checkcast #9 // class java/lang/String
24: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
27: return
我們可以看到在21行插入了一條型別轉換的指令。
型別擦除還帶來了另一個問題,如果我們有一個類繼承了泛型類並重寫了父類的方法:
public class SubGenerics extends Generics<String> {
@Override
public void setValue(String value) {
System.out.println(value);
}
public static void main(String[] args) {
Generics<String> generics = new SubGenerics();
generics.setValue("ss");
}
}
因為型別擦除所以SubGenerics
實際上有兩個setValue
方法,SubGenerics
自己的setValue(String value)
方法和從Generics
繼承來的setValue(Object value)
方法。例子中的generics
引用的是SubGenerics
物件,所以我們希望呼叫的是SubGenerics.setValue
。為了保證正確的多型性,編譯器在SubGenerics
類中生成了一個橋方法
:
public void setValue(Object value) {
setValue((String) value);
}
我們可以編譯驗證下:
Compiled from "SubGenerics.java"
public class generics.SubGenerics extends generics.Generics<java.lang.String> {
public generics.SubGenerics();
Code:
0: aload_0
1: invokespecial #1 // Method generics/Generics."<init>":()V
4: return
public void setValue(java.lang.String);
Code:
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: aload_1
4: invokevirtual #3 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
7: return
public static void main(java.lang.String[]);
Code:
0: new #4 // class generics/SubGenerics
3: dup
4: invokespecial #5 // Method "<init>":()V
7: astore_1
8: aload_1
9: ldc #6 // String ss
11: invokevirtual #7 // Method generics/Generics.setValue:(Ljava/lang/Object;)V
14: return
public void setValue(java.lang.Object);
Code:
0: aload_0
1: aload_1
2: checkcast #8 // class java/lang/String
5: invokevirtual #9 // Method setValue:(Ljava/lang/String;)V
8: return
}
引用《Java核心技術 卷一》
總之,需要記住有關 Java 泛型轉換的事實:
1.虛擬機器中沒有泛型,只有普通的類和方法。
2.所有的型別引數都用它們的限定型別替換。
3.橋方法被合成來保持多型。
4.為保持型別安全性,必要時插人強制型別轉換。
約束與侷限性
- 型別變數不能是基本變數,比如
int,double
等。應該使用它們的包裝類Integer,Double
。 - 虛擬機器中沒有泛型,所以不能使用執行時的型別查詢,比如
if (generics instanceof Generics<String>) // Error
- 因為型別擦除的原因,不能建立泛型類陣列,比如
Generics<String>[] generics = new Generics<String>[10]; // Error
- 不能例項化型別變數,比如
new T(...) new T[...] 或 T.class
萬用字元型別
萬用字元型別和上文中的型別變數的限定有些類似,區別是萬用字元型別是運用在宣告的時候而型別變數的限定是在定義的時候。比如萬用字元型別Generics<? extends List>
代表任何泛型Generics
型別的型別變數是List
和List
的子類。
Generics<? extends List> generics = new Generics<ArrayList>();
不過這樣宣告之後Generics
的方法也發生了變化,變成了
這樣就導致了不能呼叫setValue
方法
而getValue
方法是正常的
超型別限定
萬用字元限定還可以限定超類,比如萬用字元型別Generics<? super ArrayList>
代表任何泛型Generics
型別的型別變數是ArrayList
和ArrayList
的超類。
Generics<? super ArrayList> generics = new Generics<List>();
同樣的,Generics
的方法也發生了變化,變成了
呼叫getValue
方法只能賦值給Object
變數
呼叫setValue
方法只能傳入ArrayList
和ArrayList
的子類,超類List,Object
等都不行
反射和泛型
雖然因為型別擦除,在虛擬機器裡是沒有泛型的。不過被擦除的類還是保留了一些關於泛型的資訊,可以使用反射相關的Api
來獲取。
類似地,看一下泛型方法
public static <T extends Comparable<? super T>> T min(T[] a)
這是擦除後
public static Comparable min(Coniparable[] a)
可以使用反射 API 來確定:
- 這個泛型方法有一個叫做
T
的型別引數。 - 這個型別引數有一個子型別限定, 其自身又是一個泛型型別。
- 這個限定型別有一個萬用字元引數。
- 這個萬用字元引數有一個超型別限定。
- 這個泛型方法有一個泛型陣列引數。
後記
週一就建好的草稿,到了星期天才寫好,還是刪掉了一些小節情況下,怕是拖延症晚期了……不過也是因為泛型的內容夠多,雖然日常業務裡很少自己去寫泛型相關的程式碼,但是在閱讀類庫原始碼時要是不懂泛型就寸步難行了,特別是集合相關的。這次的大部分內容都是《Java核心技術 卷一》裡的,這可是本關於Java
基礎的好書。不過還是老規矩,光讀可不行,還是要用自己的語言記錄下來。眾所周知,人類的本質是復讀機,把好書裡的內容重複一遍,就等於我也有責任了!