Java基礎知識掃盲(四)——泛型

weixin_34146805發表於2018-08-29

泛型程式設計意味著程式碼可被不同型別的物件所重用

萬用字元(wildcard type),使用大寫。在Java苦衷,使用變數E表示集合的元素型別,K和V分別表示表的關鍵字和值的型別,T/U/S表示“任意型別”

泛型類

可以看作普通類的工廠。

public class Pair<T> {
  private T min;
  private T max;
  
  public Pair() {
    min = null;
    max = null;
  }
  
  public Pair(T min, T max) {
    this.min = min;
    this.max = max;
  }
  
  public T getMin() {return min; }
  
  public void setMin(T min) {this.min = min;}
  
  public T getMax() {return max;}
  
  public void setMax(T max) {this.max = max;}
}

泛型方法

型別變數放在修飾符之後,返回型別之前。
泛型方法可以定義在普通類中,也可以定義在泛型類中。

public static <T> T getMiddle(T... a) {
  return a[a.length / 2];
}

宣告泛型型別呼叫:
System.out.println(ArrayAlg.<Integer> getMiddle(1, 2, 3));

10155679-0f49543cb398fe80.png
宣告泛型型別.png

未宣告泛型型別呼叫:
System.out.println(ArrayAlg. getMiddle(1.0, 2, 3));// 注意這裡是1.0,2,3,自動封裝為Number型別。

10155679-5715baa7a63845ac.png
未宣告泛型型別.png

多型別呼叫:
System.out.println(ArrayAlg. getMiddle("hello", 2, 3));

10155679-91585d5b35c4541d.png
多型別時抽象為Serializable.png

型別變數的限定

對類/方法的類形變數加以約束。通過對型別變數 T 設定限定(bound) 實現。例如下面這段程式碼,要獲取最小值,需要保證T實現了Comparable介面關鍵字extends

public static <T extends Comparable> T min(T[] a){
  if (a == null || a.length == 0)
    return null;
  T smallest = a[0];
  for (T num : a){
    if (smallest.compareTo(num) > 0)
      smallest = num;
  }

  return smallest;
}

為什麼不是implements呢?記法<T entends BoundingType>
(1)表示T應該是繫結型別的子型別,T和繫結型別可以是類,也可以是介面。選擇關鍵字 extends 的原因是更接近子類的概念。
(2)多個限定型別,用“&”分隔:
T extends Comparable & Serializable
(3)多個型別變數,用“,”分割
(4)可以擁有多個介面超型別, 但限定中至多有一個類。如果用一個類作為限定,它必須是限定列表中的第一個

泛型程式碼和虛擬機器

虛擬機器沒有泛型型別物件——所有物件都是普通類。
型別擦除
定義的泛型型別,都會自動提供一個相應的原始型別(raw type)。原始型別名即刪掉泛型型別後的名字。擦除型別變數,並替換為限定型別(無限定型別的變數用Object)。例如Pair<T>的原始型別:

public class Pair {
  private Object min;
  private Object max;
  
  public Pair() {
    min = null;
    max = null;
  }
  
  public Pair(Object min, Object max) {
    this.min = min;
    this.max = max;
  }
  
  public Object getMin() {return min; }
  
  public void setMin(Object min) {this.min = min;}
  
  public Object getMax() {return max;}
  
  public void setMax(Object max) {this.max = max;}
}

有限定型別的情況:

public class Interval <T extends Comparable & Serializable> implements Serializable{
  private T lower;
  private T upper;

  public Interval (T first, T second){
    if (first.compareTo(second) <= 0) {
      lower = first;
      upper = second;}
    else {
      lower = second;
      upper = first;
    }
  }
}

原始型別Interval,如下:

public class Interval implements Serializable{
  private Comparable lower;
  private Comparable upper;

  public Interval (Comparable first, Comparable second){
    if (first.compareTo(second) <= 0) {
      lower = first;
      upper = second;}
    else {
      lower = second;
      upper = first;
    }
  }
}

切換限定class Interval <T extends Serializable & Comparable >後會發生什麼?
編譯器在必要時要向 Comparable 插入強制型別轉換。為了提高效率, 應該將標籤(tagging) 介面(即沒有方法的介面)放在邊界列表的末尾。

翻譯泛型表示式
擦除getFirst方法的返回型別,並返回Object型別的情況:

Pair<Employee> buddies = . . .;
Employee buddy = buddies.getFirst();

編譯器自動插入Employee的強制型別轉換。

翻譯泛型方法
擦除型別引數,留下限定型別的情況:

public static <T extends Comparable〉T min(T[] a)
...
// 型別引數擦除後變為
public static Comparable min(Comparable[] a)
...
// DateInterval 繼承 Pair 所用型別為LocalDate 
class DateInterval extends Pair{ // after erasure
  public void setSecond(LocalDate second)
}

同時在Pair中繼承到的setSecond方法為:

 public void setSecond(Object second)

考慮下列語序:

DateInterval interval = new DateInterval(...);
Pair<LocalDate> pair = interval;
pair.setSecond(new LocalDate());

需要對setSecond的呼叫具有多型性。那麼需要編譯器在DateInterval 類中生成一個橋方法。

public void setSecond(Object second) {
   setSecond((Date) second); 
 }

即實際上呼叫的為DateInterval.setSecond(Date)方法,這個方法是合成橋方法。

注意:

  • VM中沒有泛型,只有普通的類和方法
  • 所有的型別引數都用他們的限定型別/Object替換
  • 橋方法被合成來保持多型
  • 為保持型別安全性,必要時插入強制型別轉換。

約束及侷限性

  • 不能用基本型別代替型別引數
    Pair<Double>成立,Pair<double>不成立。
    原因:型別擦除。擦除之後,Pair含有Object型別的域。 而 Object 不能儲存 double 值。


    10155679-73a407372356e884.png
    Object 不能儲存 double 值.png
  • 檢查型別只適用於原始型別
    即檢查mm是否是任意型別的一個Pair。同樣的道理, getClass 方法總是返回原始型別

    if (mm instanceof Pair){ // 寫成Pair<String> Pair<T>均會報錯
      System.out.println("yes");
    }
    
10155679-eaccbe18734f51c4.png
getClass 方法總是返回原始型別.png
  • 不能建立引數化型別的陣列
    可以宣告通配型別的陣列, 然後進行型別轉換
Pair<String>[] pair0 = new Pair<String>[10]; // ERROR
Pair<String>[] pair = (Pair<String>[]) new Pair<?>[10];// CORRECT
  • 不能構造泛型陣列
    如下:直接構造陣列例項是會報錯的。通過強制轉換方式,執行時當Object[]引用給Comparable[]變數時,會發生類轉換異常
 T[] mm = new T[2]; // ERROR
  ...改造為
public static <T extends Comparable> T[] minmax(T... a){
  T[] mm = (T[]) new Object[2]; // Compiles with warning,
  mm[0] = a[0];
  mm[1] = a[0];
  for (T word : a) {
    if (mm[0].compareTo(word) > 0)
      mm[0] = word;
    if (mm[1].compareTo(word) < 0)
      mm[1] = word;
  }
  return mm;
}
10155679-893220c936f2c807.png
類轉換異常.png

調整minmax方法為:

public static <T extends Comparable> T[] minmax(IntFunction<T[]> constr,T... a){
// minmax 方法使用這個引數生成一個有正確型別的陣列
  T[] mm = constr.apply(2);
...
}
...
String[] strings = Pair.minmax(String[]::new,"Hello","How","Are","You");
  • Varargs警告
    既然Java 不支援泛型型別的陣列,我們傳遞一個泛型型別陣列例項:
public static <T> void addAll(Collection<T> collection, T... ts) {
  for (T t : ts) {
    collection.add(t);
  }
}
10155679-dfb347329f522f4a.png
會出現Varargs警告資訊.png

為addAll方法新增下列註解之一即可:

@SafeVarargs
@SuppressWarnings("unchecked")
  • 不能例項化型別變數
    即 new T();是不存在的,本意是不希望呼叫new Object()的,利用Supplier優化:
public static <T> Pair<T> makePair(Supplier<T> constr) {
  return new Pair<>(constr.get(), constr.get());
}
...
Pair<String> pair1 = Pair.makePair(String::new);
  • 既不能丟擲也不能捕獲泛型類物件。實際上, 甚至泛型類擴充套件 Throwable 都是不合法的。

  • 注意擦除後的衝突。要想支援擦除的轉換, 就需要強行限制一個類或型別變數不能同時成為兩個介面型別的子類, 而這兩個介面是同一介面的不同引數化。

繼承規則

問:Pair<Manager> 是Pair<Employee> 的一個子類嗎?
答:不是。

無論S和T有什麼聯絡,通常, Pair<S> 與 Pair<T> 沒有什麼聯絡。

泛型類可以擴充套件或實現其他的泛型類。就這一點而言,與普通的類沒有什麼區別。 例如, ArrayList<T> 類實現 List<T> 介面。這意味著, 一個 ArrayList<Manager> 可以被轉換為一個 List<Manager>。但是, 如前面所見, 一個 ArrayList<Manager> 不是一個ArrayList <Employee> 或 List<Employee>

10155679-0dc3f75b7a486b67.png
image.png

萬用字元型別

1.子型別限定。

Pair<? extends Employee>

表示任何泛型 Pair 型別,型別引數是 Employee 的子類,如Pair<Manager>

10155679-7f5683c1af97ca80.png
萬用字元的子型別關係.png

2.超型別限定

? super Manager

這個萬用字元限制為 Manager 的所有超型別。

10155679-8529e83318adc875.png
帶有超型別限定的萬用字元.png

例如:
計算一個 String 陣列的最小值,T 就是 String型別的, 而 String 是Comparable<String> 的子型別。

public static <T extends Comparable<T>> min(T[] a)...

處理一個 LocalDate 物件的陣列時, 會出現一個問題。
LocalDate 實現了 ChronoLocalDate, 而 ChronoLocalDate 擴充套件了 Comparable<ChronoLocalDate>。因此, LocalDate 實現的是 Comparable<ChronoLocalDate> 而不是 Comparable<LocalDate>。這種情況下,超型別來救助:

public static <T extends Comparable<? super T>> T min(T[] a) ...

int compareTo(? super T)

3.無限定萬用字元
Pair<?>

Pair<?> 和 Pair 本質的不同在於: 可以用任意 Object 物件呼叫原始 Pair 類的 setObject 方法。一般用來測試一個Pair 是否包含一個null引用,不需要實際的型別。

反射和泛型

  1. 泛型Class類,Class<T>。例如String.class就是一個Class<String>類的物件
  2. 使用 Class<T> 引數進行型別匹配。例如下例執行makePair(Employee.class)
public static <T> Pair<T> makePair(Class<T> tClass) throws InstantiationException,IllegalAccessException{
  return new Pair<>(tClass.newInstance(), tClass.newInstance());
}
  1. 虛擬機器中的泛型型別資訊
public static <T extends Comparable<? super T>>T min(T[] a)
... 擦除後
public static Comparable min(Comparable[] a)

使用反射 API 來確定:

  • 有一個T的型別引數。<T>
  • T有一個子型別限定。<T extends Comparable>
  • 限定型別有一個萬用字元引數。?
  • 萬用字元引數有一個超類限定。<? super T>
  • 有一個泛型陣列引數。T[]

相關文章