《Java核心技術(卷1)》筆記:第8章 泛型程式設計

  1. (P 327)「菱形」語法:

    ArrayList<String> files = new ArrayList<>();
    // Java 9 擴展了菱形語法的使用範圍,例如:現在可以對匿名子類使用菱形語法
    ArrayList<String> passwords = new ArrayList<>() {
        public String get(int n) {
            return super.get(n).replaceAll(".", "*");
        }
    }
    
  2. (P 328)定義泛型類:

    public class Pair<T, U> {
        ...
    }
    

    常見的做法是類型變數使用大寫字母,而且很簡短:

    • E表示集合的元素類型
    • KV分別表示表的的類型
    • TUS表示任意類型
  3. (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
    
  4. (P 332)類型變數的限定

    T是限定類型(bounding type)的子類型:

    <T extends BoundingType>
    

    一個類型變數或通配符可以有多個限定,限定類型用「&」分隔,而逗號用來分隔類型變數

    <T extends BoundingType1 & BoundingType2>
    

    可以擁有多個介面超類型,但最多有一個限定可以是類。如果有一個類作為限定,它必須是限定列表中的第一個限定

  5. (P 333)類型擦除:無論何時定義一個泛型類型,都會自動提供一個相應的原始類型。這個原始類型的名字就是去掉類型參數後的泛型類型名。類型變數會被擦除,並替換為其限定類型(或者,對於無限定的變數則替換為Object)

  6. (P 334)為了提高效率,應該將標籤介面(即沒有方法的介面)放在限定列表的末尾

  7. (P 335)調用一個泛型方法時,編譯器會擦除返回類型,並插入強制類型轉換。當訪問一個泛型欄位時,也會插入強制類型轉換。

    Pair<Employee> buddies = ...;
    Employee buddy = buddies.getFirst();
    // 編譯器會做如下類似的處理
    Pair buddies = ...;	// 擦除類型參數,Pair中的所有泛型被替換為Object
    Employee buddy = (Employee) buddies.getFirst();	// 插入強制類型轉換(方法原來的返回類型被擦除變成了Object)
    
  8. (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程式碼的,但是在虛擬機中,會由參數類型返回類型共同指定一個方法。因此,編譯器可以為兩個僅返回類型不同的方法生成位元組碼,虛擬機能夠正確地處理這種情況

  9. (P 337)對於Java泛型的轉換,有如下幾個事實:

    • 虛擬機中沒有泛型,只有普通的類和方法
    • 所有的類型參數都會替換為它們的限定類型
    • 會合成橋方法來保持多態
    • 為保持類型安全性,必要時會插入強制類型轉換
  10. (P 337)在泛型程式碼和遺留程式碼之間進行互操作時,編譯器會發出一個警告,可以通過加註解@SuppressWarnings("unchecked")使之消失

    // 將泛型對象賦給原始類型對象
    Dictionary<Integer, Component> labelTable = ...;
    @SuppressWarnings("unchecked")	// 抑制編譯器的警告
    slider.setLabelTabel(labelTable);	// warning
    
    // 將原始類型對象賦給泛型對象
    @SuppressWarnings("unchecked")	// 抑制編譯器的警告
    Dictionary<Integer, Component> labelTable = slider.getLabelTable();	// warning
    
  11. (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只能用於聲明為staticfinalprivate的構造器和方法。
    • 不能實例化類型變數

      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 { ... } // 錯誤
      
  12. (P 346)具有繼承關係的類如果作為泛型類的類型參數,則這些泛型類之間沒有繼承關係(通配符類型可以解決這個問題),例如EmployeeManager具有繼承關係,但是Pair<Employee>Pair<Manager>之間沒有繼承關係。注意:數組類型Employee[]Manager[]之間具有繼承關係

  13. (P 347)總是可以將參數化類型轉換為一個原始類型

    var managerBuddies = new Pair<Manager>(...);
    Pair rawBuddies = managerBuddies;	// 合法
    
  14. (P 347)泛型類可以擴展或實現其他的泛型類。如:ArrayList<T>實現了List<T>介面,這意味著ArrayList<Manager>實現了List<Manager>介面

  15. (P 348)通配符:在通配符類型中,允許類型參數發生變化

    Pair<? extends Employee>	// 表示任何泛型Pair類型,它的類型參數是Employee的子類
    				// 如Pair<Manager>是Pair<? extends Employee>的子類
    

    其中的方法如下:

    ? extends Employee getFirst()		// 合法,可以將返回值賦給一個Employee
    void setFirst(? extends Employee)	// 這樣不可能調用這個方法,它拒絕傳遞任何特定的類型
    
  16. (P 349)超類型限定:? super Manager,這個通配符限制為Manager的所有超類型

    void setFirst(? super Manager)	// 合法,可以向方法傳遞一個Manager對象,或者其子類型的對象
    ? super Manager getFirst()	// 不能調用這個方法,它無法確定返回值的類型,只能賦給Object
    
  17. (P 350)直觀地講,帶有超類型限定的通配符允許你寫入一個泛型對象,而帶有子類型限定的通配符允許你讀取一個泛型對象

  18. (P 351)無限定通配符:在編寫不需要實際類型的方法時很有用,可讀性更好

    ? getFirst()		// 返回值只能賦給Object
    void setFirst(?)	// 不能被調用,甚至不能傳遞Object(原始的Pair類型可以,這是Pair<T>和Pair主要的不同),可以傳遞null
    
  19. (P 352)不能在編寫程式碼中使用「?」作為一種類型,必須保存?類型的變數時,可以通過編寫輔助方法(泛型方法)解決

  20. (P 353)通配符捕獲只有在非常限定的情況下才是合法的,編譯器必須能夠保證通配符表示單個確定的類型

  21. (P 356)可以使用java.lang.reflect包中的介面Type表述泛型類型的聲明,其包含以下子類:

    • Class類,描述具體類型
    • TypeVariable介面,描述類型變數
    • WildcardType介面,描述通配符
    • ParameterizedType介面,描述泛型類或介面類型
    • GenericArrayType介面,描述泛型數組