Java泛型知識(shí)點(diǎn)總結(jié)

從 Java 程序設(shè)計(jì)語(yǔ)言 1.0 版本發(fā)布以來(lái),變化最大的部分就是泛型,致使Java SE 5.0 中增加泛型機(jī)制的主要原因是為了滿足 1999 年制定的最早的 Java 規(guī)范需求之一 (JSR 14)。使用泛型機(jī)制編寫程序代碼要比那些雜亂地使用 Object 變量,然后再進(jìn)行強(qiáng)制類型轉(zhuǎn)換的代碼具有更好的安全性和可讀性。[1]

為什么要使用泛型

泛型程序設(shè)計(jì)(Generic programming)意味著編寫的代碼可以被很多不同類型的對(duì)象所重用,例如 ArrayList 類 可以存放 String 類型對(duì)象,也可以存放 Integer 類型對(duì)象

類型參數(shù)的好處

在 Java 增加泛型類之前,泛型程序設(shè)計(jì)是用繼承實(shí)現(xiàn)的,ArrayList 類只維護(hù)一個(gè) Object 引用的數(shù)組。

public class ArrayList { // before generic classes
    private Object[] elementData;
    ...
    public Object get(int i) {}
    public void add(Object 0) {}
}

這種方法存在兩個(gè)問(wèn)題:

  • 當(dāng)獲取一個(gè)值是必須進(jìn)行強(qiáng)制類型轉(zhuǎn)換:String filename = (String) files.get(0);
  • 沒(méi)有錯(cuò)誤檢查,可以向數(shù)組列表中添加任意類型的對(duì)象:files.add(new File("..."));,對(duì)于這個(gè)調(diào)用,編譯和運(yùn)行都不會(huì)報(bào)錯(cuò),然而在其他地方,如果將 get 的結(jié)果強(qiáng)制類型轉(zhuǎn)換為 String 類型,就會(huì)產(chǎn)生一個(gè)錯(cuò)誤。

泛型提供了更好的解決方案:類型參數(shù)(type parameters): ArrayList<String> files = new ArrayList<String>();

Java SE7 之后,構(gòu)造函數(shù)中可以省略泛型類型: ArrayList<String> files = new ArrayList<>();

編譯器可以很好地使用類型參數(shù), 當(dāng)調(diào)用 get 時(shí),不用進(jìn)行強(qiáng)制類型轉(zhuǎn)換,編譯器知道返回類型為 String,而且編譯器知道有類型為 String 的 add 方法,會(huì)檢查插入?yún)?shù)的類型是否一致,這些使程序具有更好的可讀性和安全性。

泛型類

一個(gè)泛型類(generic class)就是具有一個(gè)或者多個(gè)類型變量的類,參考 corejava 的示例代碼,我們只關(guān)注泛型,而不會(huì)為數(shù)據(jù)存儲(chǔ)的細(xì)節(jié)煩惱。

public class Pair<T> {
    private T first; // use the type variable
    private T second;

    public Pair() {
        first = null;
        second = null;
    }

    public Pair(T first, T second) {
        this.first = first;
        this.second = second;
    }

    public T getFirst() {
        return first;
    }

    public T getSecond() {
        return second;
    }


    public void setFirst(T first) {
        this.first = first;
    }

    public void setSecond(T second) {
        this.second = second;
    }
}

類定義中的類型變量(type parameters,示例代碼中為T)制定方法的返回類型以及域和局部變量的類型,泛型類可以看成普通類的工廠。

// 自定義泛型類的簡(jiǎn)單使用
public class PairTest1 {
    public static void main(String[] args) {
        String[]  words = {"Mary", "had", "little", "lamb"};
        Pair<String> mm = ArrayAlg.minmax(words);
        System.out.println("min = " + mm.getFirst()); // min = Mary
        System.out.println("max = " + mm.getSecond()); // max = lamb
    }
}

class ArrayAlg {
    public static Pair<String> minmax(String[] a) {
        if (a == null || a.length == 0)  return null;

        String min = a[0];
        String max = a[0];
        for (int i = 0; i < a.length; i++) {
            if (min.compareTo(a[i]) > 0) min = a[i];
            if (min.compareTo(a[i]) < 0) max = a[i];
        }
        return new Pair<>(min, max);
    }
}

泛型方法

泛型方法可以定義在普通類中,也可以 定義在泛型類中,其中類型變量放在修飾符后面,返回類型前面。

class ArrayAlg {
    public static <T> T getMiddle(T... a) {
        return a[a.length / 2];
    }
}
String middle = ArrayAlg.<String>getMiddle(words);
String middle = ArrayAlg.getMiddle(words); // 編譯器可以 自動(dòng)判斷出T的類型。

類型變量的限定

有時(shí)需要對(duì)類或方法對(duì)泛型參數(shù)進(jìn)行限定,此時(shí)可以通過(guò)使用通配符來(lái)解決。

public class PairTest2 {
    public static void main(String[] args) {
        LocalDate[] birthdays = {
                LocalDate.of(1906, 12, 9),
                LocalDate.of(1985, 3, 5),
                LocalDate.of(1406, 2, 4),
                LocalDate.of(1996, 6, 7),
        };

        Pair<LocalDate> mm = ArrayAlg.minmax(birthdays);
        System.out.println("min = " + mm.getFirst());
        System.out.println("max = " + mm.getSecond());
    }
}

class ArrayAlg {
    public static <T extends Comparable> Pair<T> minmax(T[] a) {
        if (a == null || a.length == 0)  return null;

        T min = a[0];
        T max = a[0];
        for (int i = 0; i < a.length; i++) {
            if (min.compareTo(a[i]) > 0) min = a[i]; // min = 1406-02-04
            if (min.compareTo(a[i]) < 0) max = a[i]; // max = 1996-06-07
        }
        return new Pair<>(min, max);
    }
}

<T extends BoundingType> 表示 T 應(yīng)該是綁定類型的子類型, T 和綁定類型可以是類,也可以是接口,選擇關(guān)鍵字 extends 的原因是更接近子類的概念。一個(gè)變量或通配符可以有多個(gè)限定,如 T, U extends Comparable. & Serializable。在 Java 繼承中,可以選擇多個(gè)接口超類型,但限定中至多有一個(gè)類,如果用一個(gè)類作為限定,它必須是限定列表 中的第一個(gè)。

泛型代碼和虛擬機(jī)

虛擬機(jī)沒(méi)有泛型類型對(duì)象——所有對(duì)象都屬于普通類。

類型擦除

無(wú)論何時(shí)定義一個(gè)泛型類型,都自動(dòng)提供了一個(gè)相應(yīng)的原始類型(raw type),原始類型的名字就說(shuō)是刪去類型參數(shù)后的反響類型名。擦除(erased)類型變量,并替換為限定類型(無(wú)限定的變量用Object),如之前在繼承與多態(tài)中用的示例代碼,在擦除后的原始類型如下:

public class Pair {
    private Object first;
    private Object second;

    public Pair2(Object first, Object second) {
        this.first = first;
        this.second = second;
    }

    public Object getFirst() {
        return first;
    }

    public Object getSecond() {
        return second;
    }

    public void setFirst(Object first) {
        this.first = first;
    }

    public void setSecond(Object second) {
        this.second = second;
    }
}

因?yàn)?T 是一個(gè)無(wú)限定變量,所以直接用 Object 替換。在程序中可以包含不同類型的 Pair,如 Pair<String>Pair<LocalDate>, 而擦除類型之后就變成原始的 Pair 類型。

泛型擦除的規(guī)則為:原始類型用第一個(gè)限定的類型變量來(lái)替換,如果沒(méi)有給定限定就用 Object 來(lái)替換。我們看下面的例子。

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

    public Interval(T lower, T upper) {
        if (lower.compareTo(upper) > 0) {
            this.lower = lower;
            this.upper = upper;
        } else {
            this.upper = lower;
            this.lower = upper;
        }
    }
}

在泛型擦除之后,原始類型如下:

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

    public Interval(Comparable lower, Comparable upper) {
        if (lower.compareTo(upper) > 0) {
            this.lower = lower;
            this.upper = upper;
        } else {
            this.upper = lower;
            this.lower = upper;
        }
    }
}

如果寫出 class Interval<T extends Serializable & Comparable>,那么原始類型會(huì)用 Serializable 來(lái)代替 T,而編譯器在必要時(shí)要向 Comparable 插入強(qiáng)制類型轉(zhuǎn)換。所以為了提高效率,應(yīng)該將標(biāo)簽(tagging)接口(即沒(méi)有方向的接口)放在邊界列表的末尾。

翻譯泛型表達(dá)式

當(dāng)程序調(diào)用泛型方法時(shí),如果擦除返回類型,編譯器會(huì)插入強(qiáng)制類型轉(zhuǎn)換,例如我們的Pair<T>

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

調(diào)用 getFirst 方法時(shí)編譯器把這個(gè)方法調(diào)用翻譯為兩條虛擬機(jī)指令:

  • 對(duì)原始方法 Pair.getFirst 的調(diào)用
  • 將返回的 Object 類型強(qiáng)制轉(zhuǎn)換為 Employee 類型

同樣的情況也會(huì)出現(xiàn)在存取一個(gè)泛型域時(shí),如

Employee buddy = buddies.first;

翻譯泛型方法

類型擦除也會(huì)出現(xiàn)在泛型方法中,我們看以下泛型方法的定義:

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

泛型擦除后:

public static Comparable min(Comparable[] a)

我們可以看到參數(shù) T 已經(jīng)被擦除了,只留下了限定類型 Comparable,方法的擦除會(huì)帶來(lái)兩個(gè)巨大的問(wèn)題。這里使用 oracle tutorial 中的代碼來(lái)講解[2]。

// before erasure
public class Node<T> {
    public T data;

    public Node(T data) {
        this.data = data;
    }

    public void setData(T data) {
        System.out.println("Node.setData");
        this.data = data;
    }
}

public class MyNode extends Node<Integer> {
    public MyNode(Integer data) {
        super(data);
    }

    public void setData(Integer data) {
        System.out.println("MyNode.setData");
        super.setData(data);
    }
}
// after erasure
public class Node {

    public Object data;

    public Node(Object data) { this.data = data; }

    public void setData(Object data) {
        System.out.println("Node.setData");
        this.data = data;
    }
}

public class MyNode extends Node {

    public MyNode(Integer data) { super(data); }

    public void setData(Integer data) {
        System.out.println("MyNode.setData");
        super.setData(data);
    }
}

MyNode 是 Node 的子類,并且它復(fù)寫了父類的 setData 方法。當(dāng)我們嘗試調(diào)用以下代碼。

// after erasure
MyNode mn = new MyNode(1);
Node n  = mn;  
n.setData("Hello");
Integer x = mn.data; 
// before erasure
MyNode mn = new MyNode(1);
Node n  = (MyNode) mn; // A raw type - compiler throws an unchecked warning
n.setData("Hello"); // Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
Integer x = (String) mn.data; 

看到上述代碼時(shí),第一反應(yīng)是為什么n.setData("Hello");不會(huì)報(bào)錯(cuò),按照多態(tài)的原理,此時(shí)會(huì)調(diào)用MyNode.setData(Integer data) 方法,放入一個(gè) String 為什么編譯器會(huì)沒(méi)報(bào)錯(cuò)?

先引用官方的解釋

After type erasure, the method signatures do not match. The Node method becomes setData(Object) and the MyNode method becomes setData(Integer). Therefore, the MyNode setData method does not override the Node setData method.

因?yàn)榇嬖诜盒筒脸?/strong>,在擦除后 Node 中的方法變?yōu)?Node.setData(Object data) , 很明顯 Object 與 Integer 不是同一個(gè)類型,所以 MyNode 中同時(shí)存在從 Node 中繼承過(guò)來(lái)的 Node.setData(Object data)方法,n.setData("Hello"); 實(shí)際上調(diào)用的 Node.setData(Object data), 所以在編譯時(shí)期能檢查通過(guò),這行代碼運(yùn)行時(shí)發(fā)生如下操作:

  • 調(diào)用 MyNode 類中的 setData(Object) (這個(gè)方法會(huì)被編譯器自動(dòng)改寫為橋方法)方法(因?yàn)?MyNode 從 Node 中繼承了 setData(Object)方法)
  • n 引用的對(duì)象中的 data 域 被賦值為 "Hello"
  • mn 引用與 n 同一個(gè)對(duì)象,但是這里的 data 域被期望為 Integer 類型,因?yàn)?mn 是 MyNode extends Node<Integer> 的對(duì)象,此時(shí)因?yàn)闃蚍椒ㄖ械膹?qiáng)制類型轉(zhuǎn)換而拋出ClassCastException,程序結(jié)束。

橋方法(Birdge Method)

根據(jù)多態(tài)的設(shè)計(jì)初衷,n.setData("Hello"); 應(yīng)該調(diào)用 MyNode.setData(Integer data),但是最終它調(diào)用的卻是 Node.setData(Object data),我們可以看出類型擦除(type erasure) 與多態(tài)(polymorphism) 之間存在沖突,為了保證多態(tài)的可用性,Java 編譯器會(huì)自動(dòng)生成橋方法來(lái)解決這個(gè)問(wèn)題, 如 MyNode 將會(huì)變成如下代碼。

class MyNode extends Node {

    // Bridge method generated by the compiler
    //
    public void setData(Object data) {
        setData((Integer) data);
    }

    public void setData(Integer data) {
        System.out.println("MyNode.setData");
        super.setData(data);
    }

    // ...
}

正是因?yàn)橛袠蚍椒ǖ拇嬖?,才能保證多態(tài)功能的正常使用。

橋方法還可以用在其他地方,比如一個(gè)方法覆蓋另一方法是可以指定一個(gè) 更嚴(yán)格的返回類型。

public class Employee implements Cloneable{
    public Employee clone() throws CloneNotSupportedException {
        Employee clone = (Employee) super.clone();
        return clone;
    }
}

這里其實(shí) Employee 有兩個(gè) clone 方法

Employee clone();
Object clone();

此時(shí)也需要編譯器合成橋方法,合成的橋方法調(diào)用了新定義的方法。

總結(jié)

  • 虛擬機(jī)中沒(méi)有泛型,只有普通的類和方法
  • 所有的類型參數(shù)都用他們的限定類型替換
  • 橋方法被用來(lái)合成保持多態(tài)
  • 為保持類型安全性,必要時(shí)插入強(qiáng)制類型轉(zhuǎn)換。

約束與局限性

使用泛型時(shí)也有一些約束與局限性,大部分的約束都是由類型擦除引起的。

  • 不能用基本類型實(shí)例化類型參數(shù):如不能 Pair<double>,只能 Pair<Double>原因很簡(jiǎn)單,當(dāng)類型擦除之后,只剩下 Object 類型的域, 而 Object 不能存儲(chǔ) double 的值,這樣做與 Java 語(yǔ)言中基本類型的獨(dú)立狀態(tài)相一致。

  • 運(yùn)行時(shí)類型查詢只適用于原始類型:所有的類型查詢只產(chǎn)生原始類型,如

    if (a instanceof Pair<String>) // Error
    if (a instanceof Pair<T>) // Error
    
    Pair<String> stringPair = new Pair<>();
    Pair<Employee> employeePair = new Pair<>();
    stringPair.getClass() == employeePair.getClass // true getClass方法總返回原始類型
    
  • 不能創(chuàng)建參數(shù)化類型的數(shù)組:Node<String>[] node = new Node<String>[10]; // Error,因?yàn)轭愋筒脸?node 的類型變成 Node[],可以把它轉(zhuǎn)化為 Object[] objArray = node;,數(shù)組會(huì)記住它的元素類型,如果試圖存儲(chǔ)其他的類型,如 objArray[0] = "hello";,就會(huì)拋出 ArrayStoreException異常。

  • Varargs警告:public static <T> void addAll(Collection<T> coll, T... ts) 這個(gè)方法定義會(huì)拋出警告,因?yàn)槠渲械囊粋€(gè)參數(shù)為可變參數(shù),本質(zhì)上是泛型數(shù)組,這就違反了上一條規(guī)則,Java SE 7后可以使用 @SafeVarargs 進(jìn)行消除警告。

  • 不能實(shí)例化類型變量:

    不能使用 new T(...), new T[...], T.class這些表達(dá)式,也不能使用如下構(gòu)造器:

    public Pair<T> {
      first = new T();
      second = new T();
    }
    

    比較好的解決方法為使用構(gòu)造器表達(dá)式,如

    public static <T> Pair<T> makePair(Supplier<T> constr) {
      return new Pair<>(constr.get(), constr.get());
    }
    
    // 調(diào)用
    Pair<String> p = Pari.makePair(String::new);
    

    比較傳統(tǒng)的方法是通過(guò)反射調(diào)用 Class.newInstance 方法來(lái)構(gòu)造泛型對(duì)象

    first = T.class.newInstance(); // Error,因?yàn)榇嬖诜盒筒脸?T.class會(huì)被擦除為 Object.class
    
    public static <T> Pair<T> makePair(Class<T> c1) {
      try {
        return new Pair<>(c1.newInstance(), c1.newInstance());
      } catch(Exception ex) {
        return null;
      }
    }
    
    // 調(diào)用
    Pair<String> p = Pari.makePair(String.class);
    
  • 泛型類的靜態(tài)上下文中類型變量無(wú)效:即不能在靜態(tài)域或方法中引用類型變量。

  • 不能拋出和捕獲泛型類的實(shí)例

  • 可以消除對(duì)受查異常的檢查

  • 注意擦除后的沖突

泛型類的繼承

若 Manager 是 Employee 的子類,那么 Pair<Manager> 不是 Pair<Employee> 的子類,這一限制主要是出于類型安全的考慮,考慮一下代碼:

Pair<Manager> manager = new Pair<>(cto, cfo);
Pair<Employee> employee = manager;
employee.setFirst(staff); // 將普通員工與管理者放在一起明顯破壞了程序設(shè)計(jì)的意圖

永遠(yuǎn)可以將一個(gè)參數(shù)化類型轉(zhuǎn)化為一個(gè)原始類型,例如 Pair<Employee> 是原始類型的子類型,并且泛型類可以擴(kuò)展或?qū)崿F(xiàn)其他的泛型類,如 ArrayList<T> 實(shí)現(xiàn) List<T> 接口,這意味著一個(gè) ArrayList<Manager> 可以轉(zhuǎn)換為List<Manager>, 但是一個(gè) ArrayList<Manager>ArrayList<Employee>List<Employee>之間沒(méi)有關(guān)系。

GenericExtendsRelation

通配符類型

public static void test(Pair<? extends Employee> test) // 表示任何泛型 Pair 類型,它的類型參數(shù)是 Employee 的子類 以及其本身。
genericExtendsRelation2
public static void test(Pair<? super Employee> test) // 表示任何泛型 Pair 類型,它的類型參數(shù)是 Employee 的父類 以及其本身。

直觀得講,帶有超類型限定的通配符可以向泛型對(duì)象寫入(可以用構(gòu)造器方法),帶有子類型限定的通配符對(duì)象可以從泛型對(duì)象讀取。[1]

Pair<?> 無(wú)限定通配符

本文章與github上同步,歡迎來(lái)玩,提交issue。

Reference

1.Java核心技術(shù)·卷 I(原書第10版)

2.Effects of Type Erasure and Bridge Methods

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容