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

QJY 發表於 2020-06-29
  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介面,描述泛型陣列