2.6 泛型
泛型的本質是類型參數化,解決不确定具體對象類型的問題。在面向對象程式設計語言中,允許程式員在強類型校驗下定義某些可變部分,以達到代碼複用的目的。泛型(generic)、天才(genius)、基因(gene)三個英文單詞的詞根都是gen,最神奇的是,它們無論是拼寫還是發音都十分相像,在溝通中往往比較含糊。可以這樣了解,泛型就是這些擁有天才基因的大師們發明的。
Java 在引入泛型前,表示可變類型,往往存在類型安全的風險。舉一個生活中的例子,微波爐最主要的功能是加熱食物,即加熱肉、加熱湯都有可能。在沒有泛型的場景中,往往會寫出:
class Stove {
public static Object heat(Object food) {
System.out.println(food + "is done");
return food;
}
public static void main(String[] args) {
Meat meat = new Meat();
meat = (Meat)Stove.heat(meat);
Soup soup = new Soup();
soup = (Soup)Stove.heat(soup);
}
}
為了避免給每種食材定義一個加熱方法,如heatMeat()、heatSoup() 等,将heat()的參數和傳回值定義為Object,用“向上轉型”的方式,讓其具備可以加熱任意類型對象的能力。這種方式增強了類的靈活性,但卻會讓用戶端産生困惑,因為用戶端對加熱的内容一無所知,在取出來時進行強制轉換就會存在類型轉換風險。泛型則可以完美地解決這個問題。
泛型可以定義在類、接口、方法中,編譯器通過識别尖括号和尖括号内的字母來解析泛型。在泛型定義時,約定俗成的符号包括:E 代表Element,用于集合中的元素;T 代表the Type of object,表示某個類;K 代表Key、V 代表Value,用于鍵值對元素。我們用一個示例徹底地記住泛型定義的概念,對泛型不再有恐懼心理。如果下面代碼編譯出錯,請指出編譯出錯的位置在哪裡:
public class GenericDefinitionDemo<T> {
static <String, T, Alibaba> String get(String string, Alibaba alibaba) {
return string;
}
public static void main(String[] args) {
Integer first = 222;
Long second = 333L;
// 調用上方定義的get 方法
Integer result = get(first, second);
}
}
事實上,以上代碼編譯正确且能夠正常運作,get() 是一個泛型方法,first 并非是
java.lang.String 類型,而是泛型辨別<String>,second 指代 Alibaba。get() 中其他有被用到的泛型符号并不會導緻編譯出錯,類名後的T 與尖括号内的T 相同也是合法的。當然在實際應用時,并不會存在這樣的定義方式,這裡隻是期望能夠對以下幾點加深了解:
(1)尖括号裡的每個元素都指代一種未知類型。String 出現在尖括号裡,它就不是java.lang.String,而僅僅是一個代号。類名後方定義的泛型<T> 和get() 前方定義的<T> 是兩個指代,可以完全不同,互不影響。
(2)尖括号的位置非常講究,必須在類名之後或方法傳回值之前。
(3)泛型在定義處隻具備執行Object 方法的能力。是以想在get() 内部執行string.longValue() + alibaba.intValue() 是做不到的,此時泛型隻能調用Object 類中的方法,如toString()。
(4)對于編譯之後的位元組碼指令,其實沒有這些花頭花腦的方法簽名,充分說明了泛型隻是一種編寫代碼時的文法檢查。在使用泛型元素時,會執行強制類型轉換:
INVOKESTATIC com/alibaba/easy/coding/generic/GenericDefinitionDemo.get
(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
CHECKCAST java/lang/Integer
這就是坊間盛傳的類型擦除。CHECKCAST 指令在運作時會檢查對象執行個體的類型是否比對,如果不比對,則抛出運作時異常ClassCastException。與C++ 根據模闆類生成不同的類的方式不同,Java 使用的是類型擦除的方式。編譯後,get() 的參數是兩個Object,傳回值也是Object,尖括号裡很多内容消失了,參數中也沒有String 和Alibaba 兩個類型。資料傳回給Integer result 時,進行了類型強制轉化。是以,泛型就是在編譯期增加了一道檢查而已,目的是促使程式員在使用泛型時安全放置和使用資料。使用泛型的好處包括:
-
- 類型安全。放置的是什麼,取出來的自然是什麼,不用擔心會抛出ClassCastException 異常。
- 提升可讀性。從編碼階段就顯式地知道泛型集合、泛型方法等處理的對象類型是什麼。
- 代碼重用。泛型合并了同類型的處理代碼,使代碼重用度變高。
回到本節開頭微波爐加熱食材的例子,使用泛型可以很好地實作,示例代碼如下:
public class Stove {
public static <T> T heat(T food) {
System.out.println(food + "is done");
return food;
}
public static void main(String[] args) {
Meat meat = new Meat();
meat = Stove.heat(meat);
Soup soup = new Soup();
soup = Stove.heat(soup);
}
}
通過使用泛型,既可以避免對加熱肉和加熱湯定義兩種不同的方法,也可以避免使用Object 作為輸入和輸出,帶來強制轉換的風險。隻要這種強制轉換的風險存在,依據墨菲定律,就一定會發生ClassCastException 異常。特别是在複雜的代碼邏輯中,會形成網狀的調用關系,如果任意使用強制轉換,無論可讀性還是安全性都存在問題。
最後,泛型與集合的聯合使用,可以把泛型的功能發揮到極緻,很多程式員不清楚List、List<Object>、List<?> 三者的差別, 更加不能區分<? extends T> 與<? super T> 的使用場景。具體請參考第6.5 節。