《Java核心技術(卷1)》筆記:第8章 泛型程式設計
- 2020 年 6 月 29 日
- 筆記
- 《Java核心技術》閱讀筆記, JAVA, 閱讀筆記
-
(P 327)「菱形」語法:
ArrayList<String> files = new ArrayList<>(); // Java 9 擴展了菱形語法的使用範圍,例如:現在可以對匿名子類使用菱形語法 ArrayList<String> passwords = new ArrayList<>() { public String get(int n) { return super.get(n).replaceAll(".", "*"); } }
-
(P 328)定義泛型類:
public class Pair<T, U> { ... }
常見的做法是類型變數使用大寫字母,而且很簡短:
E
表示集合的元素類型K
、V
分別表示表的鍵和值的類型T
、U
、S
表示任意類型
-
(P 330)定義泛型方法:類型變數放在修飾符的後面,並在返回類型的前面
class ArrayAlg { public static <T> T getMiddle(T... a) { ... } }
調用泛型方法:
String middle = ArrayAlg.<String>getMiddle("John", "Q.", "Public"); // 大多數情況下,可以省略類型參數 String middle = ArrayAlg.getMiddle("John", "Q.", "Public"); // 編譯器將參數的類型與泛型類型T進行匹配,推斷出T一定是String
-
(P 332)類型變數的限定
T
是限定類型(bounding type)的子類型:<T extends BoundingType>
一個類型變數或通配符可以有多個限定,限定類型用「&」分隔,而逗號用來分隔類型變數
<T extends BoundingType1 & BoundingType2>
可以擁有多個介面超類型,但最多有一個限定可以是類。如果有一個類作為限定,它必須是限定列表中的第一個限定
-
(P 333)類型擦除:無論何時定義一個泛型類型,都會自動提供一個相應的原始類型。這個原始類型的名字就是去掉類型參數後的泛型類型名。類型變數會被擦除,並替換為其限定類型(或者,對於無限定的變數則替換為Object)
-
(P 334)為了提高效率,應該將標籤介面(即沒有方法的介面)放在限定列表的末尾
-
(P 335)調用一個泛型方法時,編譯器會擦除返回類型,並插入強制類型轉換。當訪問一個泛型欄位時,也會插入強制類型轉換。
Pair<Employee> buddies = ...; Employee buddy = buddies.getFirst(); // 編譯器會做如下類似的處理 Pair buddies = ...; // 擦除類型參數,Pair中的所有泛型被替換為Object Employee buddy = (Employee) buddies.getFirst(); // 插入強制類型轉換(方法原來的返回類型被擦除變成了Object)
-
(P 335)橋方法:用來解決多態調用和類型擦除的衝突
方法的擦除會帶來兩個問題,考慮如下程式碼:
class DateInterval extends Pair<LocalDate> { // (偽)重寫父類中的方法 // 之所以這裡加個「偽」字,是因為父類的類型參數會被編譯器擦除,變成Object,所以這裡實際上是重載了父類中的方法,只是看起來像重寫 public void setSecond(LocalDate second) { ... } // 這個類中,除了上面的那個外,還存在一個從父類繼承的方法 public void setSecond(Object second); }
這樣在多態調用時會產生問題:
DateInterval interval = ...; Pair<LocalDate> pair = interval; pair.setSecond(aDate); // 這裡調用的是哪個方法呢?類型擦除和多態發生了衝突 // 如果編譯器什麼都不做,將調用Pair.setSecond(Object),因為Pair中只存在這一個setSecond方法 // 而我們希望進行多態調用,即調用DateInterval.setSecond(LocalDate)
為了解決這個問題,編譯器會在子類中生成一個橋方法:
class DateInterval extends Pair<LocalDate> { // (偽)重寫父類中的方法 public void setSecond(LocalDate second) { ... } // 編譯器生成的橋方法,重寫了父類的setSecond方法 public void setSecond(Object second) { setSecond((LocalDate) second); // 調用上面的那個setSecond方法 } }
另外,還有一個問題,考慮如下程式碼:
class DateInterval extends Pair<LocalDate> { // (偽)重寫父類中的方法 public LocalDate getSecond() { ... } // 同理,編譯器會生成橋方法,以便進行多態調用 public Object getSecond() { return (LocalDate) getSecond(); // 這裡調用的是哪個方法呢?方法重載時要求參數類型不同,但是這裡兩個getSecond方法都沒有參數,似乎不合法 } }
程式設計師是不能這樣編寫Java程式碼的,但是在虛擬機中,會由參數類型和返回類型共同指定一個方法。因此,編譯器可以為兩個僅返回類型不同的方法生成位元組碼,虛擬機能夠正確地處理這種情況
-
(P 337)對於Java泛型的轉換,有如下幾個事實:
- 虛擬機中沒有泛型,只有普通的類和方法
- 所有的類型參數都會替換為它們的限定類型
- 會合成橋方法來保持多態
- 為保持類型安全性,必要時會插入強制類型轉換
-
(P 337)在泛型程式碼和遺留程式碼之間進行互操作時,編譯器會發出一個警告,可以通過加註解
@SuppressWarnings("unchecked")
使之消失// 將泛型對象賦給原始類型對象 Dictionary<Integer, Component> labelTable = ...; @SuppressWarnings("unchecked") // 抑制編譯器的警告 slider.setLabelTabel(labelTable); // warning // 將原始類型對象賦給泛型對象 @SuppressWarnings("unchecked") // 抑制編譯器的警告 Dictionary<Integer, Component> labelTable = slider.getLabelTable(); // warning
-
(P 338)限制與局限性:
-
不能用基本類型實例化類型參數
Pair<double> pair = ...; // 不合法,double是基本類型
-
運行時類型查詢只適用於原始類型
if (a instanceof Pair<String>) // 錯誤 if (a instanceof Pair<T>) // 錯誤 if (a instanceof Pair) // 正確 Pair<String> pair = (Pair<String>) a; // 錯誤
getClass
方法總是返回原始類型Pair<String> stringPair = ...; Pair<Employee> employeePair = ...; if (stringPair.getClass() == employeePair.getClass()) // 比較結果為true,兩個getClass調用都返回Pair.class
-
不能創建參數化類型的數組(可以聲明,但不能創建)
var table = new Pair<String>[10]; // 錯誤 var table = (Pair<String>[]) new Pair<?>[10]; // 可以,但是結果將是不安全的
如果需要收集參數化類型對象,簡單地使用
ArrayList
更安全、有效var table = new ArrayList<Pair<String>>(); // 合法
-
Varargs警告:向參數個數可變的方法傳遞一個泛型類型的實例,編譯器會發出一個警告,可以使用
@SuppressWarnings("unchecked")
或者@SafeVarargs
註解來抑制這個警告@SafeVarargs public static <T> void addAll(Collection<T> coll, T... ts) // 調用這個方法時,虛擬機必須要創建T類型的數組ts // 這違反了前面的規則,但此時編譯器只會發出一個警告
- 對於任何只需要讀取參數數組元素的方法,都可以使用
@SafeVarargs
註解 @SafeVarargs
只能用於聲明為static
、final
或private
的構造器和方法。
- 對於任何只需要讀取參數數組元素的方法,都可以使用
-
不能實例化類型變數
public Pair() { first = new T(); // 錯誤 second = new T(); // 錯誤 }
Java 8之後,最好的解決辦法:讓調用者提供一個構造器表達式
public static <T> Pair<T> makePair(Supplier<T> constr) { return new Pair<>(constr.get(), constr.get()); } Pair<String> p = Pair.makePair(String::new);
傳統的解決方法:通過反射調用
Constructor.newInstance
方法來構造泛型對象first = T.class.getConstructor().newInstance(); // 錯誤,T被擦除為Object public static <T> Pair<T> makePair(Class<T> cl) { try { return new Pair<>(cl.getConstructor().newInstance(), cl.getConstructor().newInstance()); } catch (Exception e) { return null; } } Pair<String> p = Pair.makePair(String.class);
-
不能構造泛型數組
public static <T extends Comparable> T[] minmax(T... a) { T[] mm = new T[2]; // 錯誤 ... }
-
泛型類的靜態上下文中類型變數無效:不能在靜態欄位或方法中引用類型變數
public class Singleton<T> { private static T singleInstance; // 錯誤 public static T getSingleInstance() { // 錯誤 ... } }
-
不能拋出或捕獲泛型類的實例
public class Problem<T> extends Exception { ... } // 錯誤,泛型類不能擴展Throwable try { ... } catch (T e) { ... } // 錯誤,catch子句中不能使用類型變數
-
可以取消對檢查型異常的檢查
通過使用泛型類、擦除和
@SuppressWarnings
註解,我們就能消除Java類型系統的部分基本限制(詳見P 343 ~ P 345) -
注意擦除後的衝突:例如在類中增加一個
equals
方法就可能和從Object
中繼承的equals
方法衝突倘若兩個介面類型是同一介面的不同參數化,一個類或類型變數就不能同時作為這兩個介面類型的子類
class Employee implements Comparable { ... } class Manager extends Employee implements Comparable { ... } // 錯誤
-
-
(P 346)具有繼承關係的類如果作為泛型類的類型參數,則這些泛型類之間沒有繼承關係(通配符類型可以解決這個問題),例如
Employee
和Manager
具有繼承關係,但是Pair<Employee>
和Pair<Manager>
之間沒有繼承關係。注意:數組類型Employee[]
和Manager[]
之間具有繼承關係 -
(P 347)總是可以將參數化類型轉換為一個原始類型
var managerBuddies = new Pair<Manager>(...); Pair rawBuddies = managerBuddies; // 合法
-
(P 347)泛型類可以擴展或實現其他的泛型類。如:
ArrayList<T>
實現了List<T>
介面,這意味著ArrayList<Manager>
實現了List<Manager>
介面 -
(P 348)通配符:在通配符類型中,允許類型參數發生變化
Pair<? extends Employee> // 表示任何泛型Pair類型,它的類型參數是Employee的子類 // 如Pair<Manager>是Pair<? extends Employee>的子類
其中的方法如下:
? extends Employee getFirst() // 合法,可以將返回值賦給一個Employee void setFirst(? extends Employee) // 這樣不可能調用這個方法,它拒絕傳遞任何特定的類型
-
(P 349)超類型限定:
? super Manager
,這個通配符限制為Manager的所有超類型void setFirst(? super Manager) // 合法,可以向方法傳遞一個Manager對象,或者其子類型的對象 ? super Manager getFirst() // 不能調用這個方法,它無法確定返回值的類型,只能賦給Object
-
(P 350)直觀地講,帶有超類型限定的通配符允許你寫入一個泛型對象,而帶有子類型限定的通配符允許你讀取一個泛型對象
-
(P 351)無限定通配符:在編寫不需要實際類型的方法時很有用,可讀性更好
? getFirst() // 返回值只能賦給Object void setFirst(?) // 不能被調用,甚至不能傳遞Object(原始的Pair類型可以,這是Pair<T>和Pair主要的不同),可以傳遞null
-
(P 352)不能在編寫程式碼中使用「
?
」作為一種類型,必須保存?
類型的變數時,可以通過編寫輔助方法(泛型方法)解決 -
(P 353)通配符捕獲只有在非常限定的情況下才是合法的,編譯器必須能夠保證通配符表示單個確定的類型
-
(P 356)可以使用
java.lang.reflect
包中的介面Type
表述泛型類型的聲明,其包含以下子類:Class
類,描述具體類型TypeVariable
介面,描述類型變數WildcardType
介面,描述通配符ParameterizedType
介面,描述泛型類或介面類型GenericArrayType
介面,描述泛型數組