簡介
泛型是Java SE 1.5的新特性,泛型的本質是參數化類型
( type parameters )
,也就是說所操作的資料類型被指定為一個參數,這種參數類型可以用在類、接口和方法的建立中.
在泛型類中定義參數化類型,在泛型表達式中,需要指定具體類型,即泛型在使用過程中将會被替換為具體的類型.
// 定義 參數類型
class ArrayList<E>
// 使用中 指定具體類型
ArrayList<String> list = new ArrayList<>();
原始類型(raw type)
: 就是去掉參數類型後的類,如示例中的
ArrayList
.
為什麼需要泛型
我們來看一個例子:
List list = new ArrayList();
// 下列的添加方法完全沒問題
list.add("one");
list.add(1);
// 取的時候, 如果你小心的,也沒問題
// 需要強轉, 内部是以Object引用來存放
String s = (String) list.get(0);
int i = (int) list.get(1);
// 但是如果, 不小心在擷取時 類型判斷出錯的話
for (int index = 0; index < list.size(); index++) {
String str = (String) list.get(index);
// index = 1時, 抛出java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
}
上訴方式,有兩個問題.
- 由于java是靜态語言,應該盡量避免在一個容器數組中,添加不相幹的類型執行個體.否則可能引起類型轉換錯誤.
- 這種方式,沒有類型檢查,隻能夠在運作時候,系統抛出異常後,你才會發現錯誤.
接下來使用泛型:
List<Animal> list = new ArrayList<>();
// 可以添加Animal及其子類
list.add(new Animal());
list.add(new Tiger());
// 編譯器進行類型檢査,避免插人錯誤類型的對象
// 編譯時期報錯,
list.add("one");
可以看出,泛型隻允許添加 聲明的類及其子類,其他無關類無法加入到list中,并且嘗試将其他類型加入清單,将在編譯時直接報錯.
由此可以看出泛型的特點:
- 能夠對類型進行限定
- 在編譯期對類型進行檢查,編譯時報錯
- 對于擷取明确的限定類型,無需進行強制類型轉化
- 具有良好的可讀性和安全性
泛型類
一個泛型類 ( generic class ) 就是具有一個或多個類型變量的類.定義的變量用尖括号
<>
括起來,放在類名的後面.
public class Holder<T> {
private T obj;
public Holder(T t) {
obj = t;
}
public void put(T t) {
obj = t;
}
public T get() {
return obj;
}
}
泛型定義的類型變量,可以在 成員變量, 方法參數, 局部變量, 方法傳回值中使用.
要注意的是,靜态變量和靜态方法中,不能使用類中定義的泛型參數.
這裡的
T
可以代表任意類型(Object或其子類),需要注意的是,基本資料類型不能夠使用泛型,需要使用它們對應的包裝類(wrapper Class)
用具體的類型替換類型變量就可以執行個體化泛型類型,如
Holder<String> holder = new Holder<>();
泛型接口
泛型接口與泛型類對比差別别是,泛型接口中不能使用 類型參數作為成員變量.
泛型類的繼承
當父類為泛型類或者接口時,子類可以使用具體類型來繼承父類,也可以使用類型參數繼承父類
public interface Parent<T> {
...
// ======
// 使用具體類型來繼承父類
public class Son implements Parent<Animal> {
...
// ======
// 使用類型參數繼承父類
public class Son<E> implements Parent<E> {
...
但是要注意, 一個類不能實作同一個泛型接口的兩種變體,由于類型擦除的原因,這兩個變體會成為縣宮廷的接口
// Error
public class Son implements Parent<Animal> {
...
// ========= Error
public class Child extends Son implements Parent<String> {
...
這種方式,
Child
是實作了
Parent<Animal>
和
Parent<String>
,是不允許的.
泛型方法
除了泛型類,還可以聲明一個泛型方法. 泛型方法可以在泛型類中聲明,也可以在普通方法中聲明.
注意的是,靜态方法中,隻能使用方法中定義的類型參數,而不能使用泛型類中的類型參數.
// 普通類中的泛型方法
public class Normal {
// 成員泛型方法
public <E> String getString(E e) {
return e.toString();
}
// 靜态泛型方法
public static <V> void printString(V v) {
System.out.println(v.toString());
}
}
// 泛型類中的泛型方法
public class Generics<T> {
// 成員泛型方法
public <E> String getString(E e) {
return e.toString();
}
// 靜态泛型方法
public static <V> void printString(V v) {
System.out.println(v.toString());
}
}
一個原則:在能達到目的的情況下,盡量使用泛型方法。即,如果使用泛型方法可以取代将整個類泛化,那麼應該有限采用泛型方法。
泛型類中的參數類型和泛型方法中的參數類型,即使聲明為相同的類型參數,如T,兩者的類型不會互相影響,甚至可以說沒有任何關聯.方法中的類型,由傳入的參數決定,與泛型類的類型無關.
// 泛型類中的類型參數 用 T 表示
public class Holder<T> {
...
// 成員泛型方法, 聲明的類型參數,也用 T 表示
public <T> String getString(T t) {
return t.toString();
}
}
public static void main(String[] args) {
// 泛型類為 Animal
Holder<Animal> holder = new Holder<>(new Animal());
// 泛型方法為 Vegetation
String s = holder.getString(new Vegetation());
System.out.println(s);
// I'm Vegetation
}
泛型方法的使用過程中,無需對類型進行聲明,它可以根據傳入的參數,自動判斷.
public class Main {
public static void main(String[] args) {
// 指定類型
Main.<String>printString("one");
// 不指定,自動推倒
Main.printString("two");
}
static <T> void printString(T t) {
System.out.println(t.toString());
}
}
類型變量的限定
對于類型變量沒有限定的泛型類或方法, 它是預設繼承自
Object
,當沒有傳入具體類型時,它有的能力隻有
Object
類中的幾個預設方法實作.
如果我們要實作一個方法, 傳入兩個參數,傳回其中大的一個,即
max()
函數.
public static void main(String[] args) {
// 傳入 4 , 2 , 自動裝箱成Integer類
int r = max(4, 2);
}
static <T> T max(T t1,T t2){
// Cannot resolve method 'compareTo(T)'
return t1.compareTo(t2) > 0 ? t1 : t2;
}
如果沒有對類型進行限定,它預設隻有
Object
能力,它沒有
compareTo
方法,是以沒有比較能力,此時,即使在調用的時候傳入可以比較的對象,
max
方法會在編譯器報錯.
此時, 我們就需要對 類型參數進行限定,讓它能夠預設擁有一些類的"能力".
public static void main(String[] args) {
// 傳入 4 , 2 , 自動裝箱成Integer類
int r = max(4, 2);
// r = 4
}
// 繼承 Comparable 的類具有比較功能,能夠比較大小 , 該函數傳回傳入的最大值
static <T extends Comparable<T>> T max(T t1, T t2) {
return t1.compareTo(t2) > 0 ? t1 : t2;
}
從代碼中可以看出,
T
被限定為
Comparable
的子類(
Comparable
類本身是泛型類,也需要對他進行類型參數聲明,否則會引發編譯警告.),是以它擁有了 父類
Comparable
有的能力,即比較功能,這樣我們才能得到正确的結果.
類型參數的限定 可以記為
<T extends BoundingType>
,由于java有單繼承類多實作接口的特點,是以還可以有多個限定.
<T extends BoundingType1 & BoundingType2 & ...>
在 Java 的繼承中, 可以擁有多個接口超類型, 但限定中至多有一個類。 如果用 一個類作為限定, 它必須是限定清單中的第一個.
泛型的實作原理
java中泛型的實作是采用
類型擦除
的方式實作.
所謂的類型擦除,就是程式在編譯階段,編譯器會對泛型變量進行擦除(
erased
)操作,并替換為限定類型 (沒有限定的變量用 Object);泛型類也将擦除為原始類型; 在泛型表達式中(泛型的使用),會将類型替換為具體的類型(此時将發生強制轉換)
下面我們通過,反編譯泛型類的方式來揭開
類型擦除
的面紗.
使用
jad -sjava Holder.class
來反編譯
Holder
類.
// 泛型類
public class Holder<T> {
private T obj;
public void put(T t) {
obj = t;
}
public T get() {
return obj;
}
}
// 泛型使用
public static void main(String[]args){
Holder<Animal> holder=new Holder<>();
holder.put(new Tiger());
// 在使用過程中沒有發生強轉
Animal animal=holder.get();
}
// ---------------------------------
// 反編譯出來的類, 它的類型被擦除為Object
public class Holder {
private Object obj;
public Holder() {
}
public void put(Object t) {
obj = t;
}
public Object get() {
return obj;
}
}
public static void main(String args[]){
Holder holder=new Holder();
holder.put(new Tiger());
// 泛型的使用過程中,使用強制轉換為目标類型
Animal animal=(Animal)holder.get();
}
// -------------------------------------
// javap 反彙編對main方法,到處的指令碼
public static void main(java.lang.String[]);
Code:
0:new #2 // class generics/Holder
3:dup
4:invokespecial #3 // Method generics/Holder."<init>":()V
7:astore_1
8:aload_1
9:new #4 // class bean/Tiger
12:dup
13:invokespecial #5 // Method bean/Tiger."<init>":()V
16:invokevirtual #6 // Method generics/Holder.put:(Ljava/lang/Object;)V
19:aload_1
20:invokevirtual #7 // Method generics/Holder.get:()Ljava/lang/Object;
23:checkcast #8 // class bean/Animal
26:astore_2
27:return
由上述反編譯的代碼可以看出,泛型類被擦除為 原始類型;
泛型類中的類型參數變量也擦除為
Object
類型;
泛型的表達式中,發生了強制轉換為目标類型.
從反彙編代碼中可以看出,
holder.get()
方法,被分解為兩條指令.
1. 原始類型調用方法,對應
invokevirtual
指令
2. Object類型強制轉換為Animal類型,對應
checkcast
如果限定為
T extends Animal
,則類型參數變量将被擦除為限定類型
Animal
// 泛型類
public class Holder<T extends Animal> {
private T obj;
...
// 反編譯類
public class Holder {
private Animal obj;
...
java泛型的局限
- 不能用基本類型執行個體化類型參數
泛型中的 類型參數在沒有限定的情況下 是預設 擦除為
Object
,而基本類型變量無法轉化為
Object
類型.
不過沒關系, java中8中基本類型,都有其對應的包裝類(Wrapper Class), 并且基本類型 使用參數傳遞是,将被 (自動裝箱)AutoBoxing 為包裝類型.
- 運作時,無法對類型參數進行檢查
由于編譯時,擦除了類型參數, 是以,所有的類型查詢隻産生原始類型.
是以,一下的語句是不可行的.
Holder<Tiger> holder = new Holder<>(new Tiger());
// ERROR 無法對類型參數進行判斷
if (holder instanceof Holder<Tiger>) ;
if (holder instanceof Holder<T>) ;
由于類型擦除,
Holder<String>
Holder<Animal>
的執行個體,擷取的類都是原始類,是一樣的,是以他們的
getClass()
方法的傳回是一樣的.
- 不能直接建立參數化類型的數組
如這樣的代碼
Holder<Animal>[] holders = new Holder<Animal>[2]
,是通過不了編譯的.
但是,可以通過以下方式來建立數組,不會報錯,隻是受到警告
// 使用原始類型而後強制轉換
Holder<Animal>[] holders = (Holder<Animal>[]) new Holder[2];
// 使用通配符而後強制轉換
Holder<Animal>[] holders = (Holder<Animal>[]) new Holder<?>[2];
- 不能夠執行個體化類型變量
這樣的語句
T t = new T()
T t = new Object()
,是通過不了編譯的,一定要在泛型表達式中申明了具體類型,才能建立.
某些情況下,我們需要建立 參數類型的變量, 那麼前提是一定要知道被建立的類型.可以通過以下兩種方式來建立:
- 反射建立
// 使用(如) newObject(Animal.class);
static <T> T newObject(Class<T> cls) throw Exception{
return cls.newInstance();
}
- jdk8以後,可使用構造器表達式
// 使用(如) newObject(Animal::new);
static <T> T newObject(Supplier<T> constr) {
return constr.get();
}
- 不能構造泛型數組
不能直接執行個體化 類型數組,如
T[] arr = new T[2]
但是可以這樣
T[] arr = new Object[2]
, 原因是數組本身也有類型,用來監控存儲在虛拟機
中的數組,這個類型會被擦除為
Object
雖然這種方式能夠建立泛型數組,但是為了類型安全起見,最好提供構造器來實作泛型資料的建立.
// 使用構造器
// 使用(如) newArray(Animal[]::new, 2);
static <T> T[] newArray(IntFunction<T[]> constr, int length) {
return constr.apply(length);
}
// 使用反射
// 使用(如) newArray(String.class, 2);
static <T> T[] newArray(Class<T> cls, int length) throws Exception {
return (T[]) Array.newInstance(cls, length);
}
- 不能在靜态變量和靜态方法中,使用泛型類中的類型參數
如以下的方式都不允許
public class Test<T> {
// Error
private static T t;
// Error
public static T test() {
T t;
}
}
- 注意擦除後的沖突
- 由于類型擦除,方法重寫時,會下列沖突.
public class Holder<T> {
public boolean equals(T t) {
...
}
由于類型擦除,該方法會被擦除為
boolean equals(Object t)
,這和Object類中的
equals
方法完全沖突了,傳回值和方法名,參數都一緻了.
這種沖突,解決方案隻能是,将方法重命名!
- 由于類型擦除,方法重載時,也可能發生沖突.
再觀察下面的代碼:
public interface Parent<T> {
T get();
}
public class Son implements Parent<Animal> {
@Override
public Animal get() {
return new Animal();
}
}
public class Main {
public static void main(String[] args) {
Parent<Animal> parent = new Son();
Animal animal = parent.get();
}
}
父類中根據類型擦除, 擁有
Object get()
方法, 子類傳入具體的類型參數,擁有
Animal get()
方法,且繼承父類方法,是以子類中同時擁有這兩個方法.
這兩個方法,方法參數相同,就隻有傳回值類型不一樣.
在java文法中,是不允許這樣的兩個方法同時在一個類中存在,會把他們認為是同一種方法(方法簽名根據方法參數和方法名來确定).
但是jvm卻能分辨,jvm的方法簽名,是通過方法參數,方法傳回值,方法名來确定的,是以jvm允許這樣的方法存在, 是以,對于這種沖突,不需要我們自己來處理, jvm通過一種稱為
Bridge Method
的方式來實作這種方式下的多态調用沖突.
感興趣的可以檢視,筆者的另一篇文章
java中多态的實作原理為什麼使用 類型擦除來實作泛型
因為,泛型提出來時,已經是java1.5的版本,java已經經曆過10年的發展,java遺留的代碼量可想而知.
為了相容這部分的(舊)代碼,而不得不采用這種方式來實作.
通配符 <?> <? extends T> <? super T>
泛型通配符解釋起來比較複雜,這裡就不進行展開,感興趣的可以檢視筆者的另一篇文章
java泛型通配符詳解及實踐