泛型概述-基本概念


泛型程序設(shè)計 (Generic programming) 意味著編寫的代碼可以被很多不同的類型的對象所重用。

原始類型(Raw Type)

下面我們會用一些例子來說明,為什么使用泛型編寫的代碼可以被不同類型的對象所重用。在沒有泛型之前 ArrayList 的代碼是這樣的:

public class RawArrayList {

    private Object[] element;

    public Object getElement(int position) {
        //...
    }
    
    public void addElement(Object element) {
        //...
    }
}
public class RawArrayListTest {
    public static void main(String[] args) {
        RawArrayList rawArrayList = new RawArrayList();
        rawArrayList.addElement("string");
        rawArrayList.addElement(new File("file"));

        String string = (String) rawArrayList.getElement(0);
        
        //此處會出現(xiàn)類型轉(zhuǎn)換異常
        String file = (String) rawArrayList.getElement(1);
    }
}

上面的代碼有兩個問題:

  1. 編譯器沒有錯誤檢查,我們可以調(diào)用 setElement("string") 方法向 RawArrayList 中放入一個 String 類型的字符串,之后我仍然可以向其中放入一個其他類型的對象,例如 setElement(new File("file"))。編譯器并不會有任何警告。
  2. 因為 RawArrayList 內(nèi)部使用 Object 數(shù)組 來存儲對象,這樣我們在獲取對象的時候就必須使用強制類型轉(zhuǎn)換,String string = (String) rawArrayList.getElement(0);,由于 RawArrayList 沒有對放入的類型做限制,所以就有可能出現(xiàn)類型轉(zhuǎn)換異常 java.lang.ClassCastException。

類型參數(shù)

泛型提供了類型參數(shù) (type parameters) 來幫助我們改善上述的代碼。
可以認(rèn)為是給 RawArrayList 中聲明一個參數(shù),這個參數(shù)就代表著列表中元素的類型,我們會在聲明 RawArrayList 的時候指明參數(shù)的具體類型。

類型參數(shù)用尖括號加任意字母表示 : <T>,字母一般為單個大寫字母,并有一定含義,例如 T(type),E(element),K(key),V(value) 等等

下面來看看用使用類型參數(shù)之后的 ArrayList:

public class ArrayListTest {
    public static void main(String[] args) {
        ArrayList<String> stringList = new ArrayList<String>();
        stringList.add("string");
//        下面一行代碼,編譯器會報錯,無法將 File 對象應(yīng)用于 String 類型的 ArrayList
//        stringList.add(new File("file"));

        String string=stringList.get(0);
        System.out.println(string);
    }
    
    // print > string
}

ArrayList<String> stringList = new ArrayList<String>(); 這一行代碼中,可以省略創(chuàng)建 ArrayList 對象的時候傳遞的參數(shù)類型如 ArrayList<String> stringList = new ArrayList<>();,編譯器可以從聲明中推斷出省略的類型。

注意尖括號不能省略,不然可能造成類型不安全的隱患( 這相當(dāng)于將原始類型的對象傳遞給泛型類型的引用 )。例如我們將之前的內(nèi)部具有 String 類型和 File 對象的 RawArrayList 傳遞給 ArrayList<String> 類型的引用,會造成什么影響呢?

 RawArrayList list=new RawArrayList();
        list.add(1);
        list.add("string");

        ArrayList<String> stringArrayList=list;
        for (String s : stringArrayList) {
            System.out.println(s);//boom
        }

很不幸會發(fā)生類型轉(zhuǎn)換異常,我們最好不要將原生類型和泛型類型這樣使用,除非你能保證類型安全。

泛型的一個目的就是盡早的發(fā)現(xiàn)可能出現(xiàn)的異常,在使用了泛型提供的類型參數(shù)之后,有兩個顯著的好處是

  1. 我們不需要自己進行類型轉(zhuǎn)換了,編譯器能推斷出返回類型,可讀性提高
  2. 編譯器會對插入數(shù)據(jù)做類型檢查,避免插入了錯誤的類型,安全性提高

好像 RawArrayList 代碼的例子沒有明顯體現(xiàn)出我們在開頭所說的,泛型代碼可以被很多不同的類型的對象所重用。

接下來在讓我們看看水果和果盤的例子:

Apple

public class Apple {
    private String name;

    public Apple(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

Orange

public class Orange {
    private String name;

    public Orange(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

現(xiàn)在我們再來定義一個果盤,用來存放水果。我們需要定義一個蘋果果盤,用來存放蘋果,再定義一個橘子果盤,用來存放橘子。

ApplePlate

public class ApplePlate {
    private List<Apple> appleList;

    public ApplePlate(List<Apple> appleList) {
        this.appleList = appleList;
    }

    public void setAppleList(List<Apple> appleList) {
        this.appleList = appleList;
    }

    public List<Apple> getAppleList() {
        return appleList;
    }
    
}

OrangePlate

public class OrangePlate {
    private List<Orange> orangePlate;

    public OrangePlate(List<Orange> orangePlate) {
        this.orangePlate = orangePlate;
    }

    public List<Orange> getOrangePlate() {
        return orangePlate;
    }

    public void setOrangePlate(List<Orange> orangePlate) {
        this.orangePlate = orangePlate;
    }
}

將蘋果放進蘋果果盤

private static void createApple() {
    //生成蘋果
    List<Apple> apples = new ArrayList<>();
    Apple apple1 = new Apple("蘋果1");
    Apple apple2 = new Apple("蘋果2");
    Apple apple3 = new Apple("蘋果3");
    apples.add(apple1);
    apples.add(apple2);
    apples.add(apple3);
    //將蘋果放入蘋果果盤
    ApplePlate applePlate = new ApplePlate(apples);

    //取出剛放入的蘋果們
    for (Apple apple : applePlate.getAppleList()) {
        System.out.println(apple.getName());
    }
}

現(xiàn)在將橘子放進橘子果盤的話,只需要按照 createApple() 方法在編寫一個 createOrange() 就可以了。

那如果現(xiàn)在我要新增一個水果類型怎么辦,我還需要對應(yīng)的再增加一個該水果類型的果盤。而且可以看到,我們水果的屬性,方法,果盤的方法,除了類型不同之外,沒什么不同。這時候就可以使用泛型來解決這個問題。

泛型類/接口

先讓我們看看泛型類的概念:

具有一個或者多個類型參數(shù)的類/接口,就是泛型類/泛型接口

在定義類的時候,我們在類名的后面加上一個形如 <T> 的類型參數(shù)。類中屬性的聲明,方法的參數(shù)類型,包括返回類型等,都可以用類型 T 替代。泛型接口與泛型類的定義相同,我們就不展開敘述了。

現(xiàn)在我們將果盤(XXXPlate)改寫為泛型類是什么樣子

public class Plate<T> {
    private List<T> fruitList;

    public Plate(List<T> fruitList) {
        this.fruitList = fruitList;
    }

    public List<T> getFruitList() {
        return fruitList;
    }

    public void setFruitList(List<T> fruitList) {
        this.fruitList = fruitList;
    }
}

現(xiàn)在我們先抽象出一個水果類 Fruit

public class Fruit {
    private String name;

    public Fruit(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

為了對比,我們新創(chuàng)建一種水果類型 Cherry 使他繼承 Fruit

public class Cherry extends Fruit {
    public Cherry(String name) {
        super(name);
    }
}

首先通過抽象將共有的屬性和方法抽象到父類中去,這樣子類只需要實現(xiàn)一個構(gòu)造函數(shù)即可。

接下來我們看看,使用了泛型之后,是如何將 Cherry 裝進果盤中去的。

public static void createCherry() {
    //生成車?yán)遄?    List<Cherry> cherryList = new ArrayList<>();
    Cherry cherry1 = new Cherry("車?yán)遄?");
    Cherry cherry2 = new Cherry("車?yán)遄?");
    Cherry cherry3 = new Cherry("車?yán)遄?");
    cherryList.add(cherry1);
    cherryList.add(cherry2);
    cherryList.add(cherry3);

    //將剛買的車?yán)遄臃湃胲嚴(yán)遄庸P
    Plate<Cherry> cherryPlate = new Plate<>(cherryList);
    for (Cherry cherry : cherryPlate.getFruitList()) {
        System.out.println(cherry.getName());
    }
}

假如我們現(xiàn)在又增加了一種水果 Pear ,這個時候我們只需要將 Plate<T> 中的類型參數(shù)指定為 Pear 這個樣子 Plate<Pear> 即可。

這就體現(xiàn)了我們上面所說的,泛型代碼可以被不同類型的對象所重用??梢赃@么認(rèn)為:我們封裝了一套數(shù)據(jù)結(jié)構(gòu)和算法,用來處理一類操作,他與具體的類型無關(guān),或者與限定的類型有關(guān),這個時候,我們就可以使用泛型,只關(guān)注具體的操作,不用關(guān)心具體的類型。

泛型方法

我們類比泛型類可以知道,泛型方法就是具有一個或者多個類型參數(shù)的方法

將類型參數(shù) <T> 放在修飾符的后面返回類型的前面,這樣我們的返回值,方法中的局部變量,參數(shù)類型都可以指定為我們聲明的 T 類型。

我們這樣定義一個泛型方法:

 public static <T> T getMiddleFruit(Plate<T> plate) {
    int middle = plate.getFruitList().size();
    return plate.getFruitList().get(middle);
}

這段代碼的意思是,獲取 Plate<T> 中間的元素,也就是獲取果盤最中間的水果。

我們可以這樣來調(diào)用它:

public static void createCherry() {
    /...省略之前創(chuàng)建水果,將水果放進果盤的操作
    Cherry middleCherry=PlateUtils.getMiddleFruit(cherryPlate);
    System.out.println(middleCherry.getName());
}

泛型限定

在回頭看我們上面定義的泛型類 Plate<T>,我們的類型參數(shù)是沒有做任何限定的,類型參數(shù) T 可以在聲明的時候被指定為任何類型。

雖然我將 Plate,定義為果盤,可以傳進來任何類型的水果,但其實由于我沒有對 T 做任何的限定,那就意味著我們在聲明的時候可以傳遞任意類型。

如下我們定義一個動物類型 Animal

public class Animal {
    private String name;

    public Animal(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

然后我們來嘗試將一群動物放進果盤中:

public static void createAnimal() {
    List<Animal> animalList = new ArrayList<>();
    Animal dog = new Animal("Dog");
    Animal cat = new Animal("Cat");
    animalList.add(dog);
    animalList.add(cat);

    Plate<Animal> animalPlate = new Plate<>(animalList);
    for (Animal animal : animalPlate.getFruitList()) {
        System.out.println(animal.getName());
    }
}

尷尬??,我們從果盤中取出一群動物,現(xiàn)在我們要修正上面的代碼,不然就亂套了,這個時候我就可以使用類型限定,對類型參數(shù)加以約束。

我們只需要將代碼改成這樣:

public class Plate<T extends Fruit> {...}

將類型限定為 Fruit 類型或者是 Fruit 的子類,這樣我們在嘗試編譯上面的createAnimal()代碼的時候,編譯器就會報錯:

  1. Error:(172, 15) java: 類型參數(shù)model.Animal不在類型變量T的范圍內(nèi)
  2. Error:(172, 37) java: 無法推斷model.plate.Plate<>的類型參數(shù)
    原因: 推論變量 T 具有不兼容的限制范圍
    等式約束條件: model.Animal
    上限: model.base.Fruit

由于我們對 Plate 能夠接收的類型做了限制,所以現(xiàn)在我們無法將一群動物 List<Animal> 放進果盤了。

泛型擦除

Java 核心技術(shù):無論何時定義一個泛型類型,都會自動提供一個原始類型(Raw Type),原始類型的名字就是刪去類型參數(shù)后的泛型類型名。擦除類型變量,并切換為限定類型,沒有限定類型則替換為 Object

請看下面的代碼:

    List<Banana> bananas = new ArrayList<>();
    Plate<Banana> bananaPlate = new Plate<>(bananas);

    List<Pear> pears = new ArrayList<>();
    Plate<Pear> pearPlate = new Plate<>(pears);
    
    System.out.println(bananaPlate.getClass());
    System.out.println(pearPlate.getClass());
    System.out.println(bananaPlate.getClass()==pearPlate.getClass());
    //print>class model.plate.Plate
    //print>class model.plate.Plate
    //print>true

在運行時,Plate<Banana>Plate<Pear> 的類型是一樣的,都是 Plate 類型,我們在代碼中指定的具體類型 BananaPear 不見了。

造成這個問題的原因就是類型擦除,類型擦除后的 Plate<T extends Fruit> 代碼是這個樣子的,為了展示類型變量被替換成為限定類型,我特意在原有的 Plate 的代碼中加入一句 ``T t;`,聲明一個 T 類型的變量 t,然后我們來看擦除后的代碼:

public class Plate {
    private Fruit t;
    private List fruitList;

    public Plate(List fruitList) {
        this.fruitList = fruitList;
    }

    public List getFruitList() {
        return fruitList;
    }

    public void setFruitList(List fruitList) {
        this.fruitList = fruitList;
    }
}

可以看到在運行時所有有關(guān)于泛型的信息全部不見了,使用泛型聲明的屬性,全都替換成了限定類型 Fruit(大家可以嘗試用 javap -c -s -p Plate
指令來反編譯字節(jié)碼,就可以看到更具體的信息)

在文章的最開始我們舉了一個有關(guān)于 ArrayList 的例子,聲明一個 String 類型的 ArrayList,我們先 add 一個字符串進去,在將該字符串取出,來看看編譯后的文件

...
 ArrayList<String> stringList = new ArrayList<>();
        stringList.add("string");
        String string=stringList.get(0);
...
image_1c55onosh1hoslgnvl81eidffe26.png-166.6kB
image_1c55onosh1hoslgnvl81eidffe26.png-166.6kB
  1. 創(chuàng)建了一個原生的 ArrayList 沒有泛型相關(guān)的信息
  2. 調(diào)用 add 方法,接收的參數(shù)類型是 Object 類型
  3. 調(diào)用 get 方法,方法的返回類型是 Object 類型
  4. 將 Object 類型轉(zhuǎn)化為 String 類型

Java 中的泛型之所以設(shè)計成編譯時泛型,就是為了兼容老代碼,能夠和之前的代碼相互操作

拿我們的 Plate 來說說這件事,來看看原生的 Plate 類型和泛型類 Plate<T> 之間是怎么相互操作并且會帶來什么影響。

public class RawAndGenericOperation {
    public static void main(String[] args) {
        List<Banana> bananas = new ArrayList<>();
        Banana banana = new Banana("banana");
        bananas.add(banana);
        Plate<Banana> bananaPlate = new Plate<>(bananas);

        Plate rawPlate = bananaPlate;

        List<Pear> pears = new ArrayList<>();
        Pear pear = new Pear("pear");
        pears.add(pear);
        rawPlate.setFruitList(pears);//

        for (Object o : rawPlate.getFruitList()) {
            System.out.println(((Banana) o).getName());
            //Exception in thread "main" java.lang.ClassCastException: model.pear.Pear cannot be cast to model.banana.Banana
        }
    }
}

在 For 循環(huán)取數(shù)據(jù)的時候??了,類型轉(zhuǎn)換異常,代碼清晰可見,是由于我們自己將 List<Pear> 傳遞給了 Plate,原生類型沒有類型檢查,造成了類型不安全的隱患,我們在將泛型類型對象傳遞給原生類型的引用的時候,這個隱患就存在了,誰知道它們會對原生類型做些什么呢。

在來看看將原生類型的對象,傳遞給泛型類型的引用:

public class RawAndGenericOperation {
    public static void main(String[] args) {

        List bananaList = new ArrayList();
        bananaList.add(new Banana("banana"));
        bananaList.add(new Pear("pear"));


        List<Banana> bananas = new ArrayList<>();
        bananas = bananaList;

        for (Banana banana : bananas) {
            System.out.println(banana.getName());
            //Exception in thread "main" java.lang.ClassCastException: model.pear.Pear cannot be cast to model.banana.Banana
        }
    }
}

依然??,由于原生類型的 List 沒有對存儲的元素做限制,我們在 BananaList 中混入了 Pear,然后將它賦值給 List<Banana>。在循環(huán)的時候出現(xiàn)了類型轉(zhuǎn)換異常,除非你能保證原生 List 中的元素類型和泛型類型保持一致,不然就不要這么做。

但是在與遺留的代碼進行銜接的時候,難免會出現(xiàn)上述的情況,但是沒有關(guān)系,這里只是失去了泛型程序設(shè)計提供的附加安全性,不會變的更壞了。

泛型的繼承關(guān)系

在之前我們定義了 Banana 類型,它是 Fruit 的子類。那么 List<Banana>List<Fruit> 的子類嗎?那么 Banana[]Fruit[] 類型的子類嗎?

在回答這兩個問題之前,我們先來看看一個概念 Java中的逆變與協(xié)變。然后我們來寫兩段代碼試試看:

public class Covariant {
    public static void main(String[] args) {
        List<Fruit> fruitList = new ArrayList<>();
        List<Banana> bananaList = new ArrayList<>();

        //編譯器提示類型不兼容
        fruitList = bananaList;

        Fruit[] fruits = new Fruit[10];
        Banana[] bananas = new Banana[10];

        //不會有任何問題
        fruits = bananas;
    }
}

由上可知 List<Banana> 沒有辦法轉(zhuǎn)化成 List<Fruit> 類型。但是 Banana[] 可以轉(zhuǎn)化為 Fruit[] 類型,劃重點在 Java 中數(shù)組是支持協(xié)變的,但是泛型是不支持協(xié)變的。假如讓泛型支持協(xié)變會怎么樣,假設(shè) List<Banana> 可以傳遞給 List<Fruit> 類型的引用會發(fā)什么呢?

List<Fruit> fruitList = new ArrayList<>();
List<Banana> bananaList = new ArrayList<>();

//假設(shè)這行代碼允許執(zhí)行
fruitList = bananaList;
        
fruitList.add(new Pear("pear"));
fruitList.add(new Fruit("fruit"));

for (Banana banana : bananaList) {
    //類型轉(zhuǎn)換異常
}

和上面的問題一樣,我們丟失了類型的安全性。那為什么數(shù)組是協(xié)變的但卻一點事情沒有呢?
首先來看看知乎中胖胖的回答Java 中數(shù)組為什么要設(shè)計為協(xié)變

public class CovariantArray {
    public static void main(String[] args) {
        Fruit[] fruits = new Fruit[3];
        Banana[] bananas = new Banana[4];

        bananas[0] = new Banana("banana1");
        bananas[1] = new Banana("banana2");
        bananas[2] = new Banana("banana3");

        fruits = bananas;
        fruits[3] = new Pear("pear");
        for (Fruit fruit : fruits) {
            System.out.println(fruit.getName());
            //Exception in thread "main" java.lang.ArrayStoreException: model.pear.Pear
        }
    }
}

數(shù)組中帶有特別的保護,數(shù)組會在創(chuàng)建的時候記住元素類型,如果后續(xù)的插入與之前的類型不匹配,虛擬機將會拋出 ArrayStoreException 異常。

數(shù)組在插入的時候就暴露出問題,如果是泛型協(xié)變的話,你就不知道什么時候會發(fā)現(xiàn)問題了。

泛型數(shù)組

在最開始你可能會編寫這樣一段代碼:

public class GenericArray {
    public static void main(String[] args) {
        Plate<Banana>[] bananaArray = new Plate<Banana>[10];
    }
}

編譯器會直接報 Error:創(chuàng)建泛型數(shù)組,我們是沒有辦法通過 new 的方式來創(chuàng)建泛型數(shù)組的,如果編譯器允許我們創(chuàng)建泛型數(shù)組會怎么樣?

public class GenericArray {
    public static void main(String[] args) {
    // 假設(shè)編譯器沒有報錯
        Plate<Banana>[] bananaArray = new Plate<Banana>[2];

        List<Banana> bananas = new ArrayList<>();
        Plate<Banana> bananaPlate = new Plate<>(bananas);
        bananaArray[0] = bananaPlate;

        List<Pear> pears = new ArrayList<>();
        Plate<Pear> pearPlate = new Plate<>(pears);

        Object[] objectArray = bananaArray;
        objectArray[1] = pearPlate;
    }
}

可以看到我們將 Plate<Banana>[] 向上轉(zhuǎn)型為 Object[],然后向其中追加一個 Plate<Pear>對象,這個時候編譯器沒有報錯,原本我們期望在運行時,數(shù)組會判斷出加入的數(shù)據(jù)類型不對從而報出 ArrayStoreException,但是被忘了類型擦除這回事,我們的 Plate<T> 全部被擦除成 Plate 類型,對于數(shù)組來說,無論你插入 Plate<Banana> 或者 Plate<Pear> 由于類型擦除,它都認(rèn)為是同一種類型。這個時候泛型數(shù)組就變得類型不安全了。

編譯器只是不允許通過 new Plate<T>[] 這種方式創(chuàng)建數(shù)組,我們依然可以通過其他方式來得到一個泛型數(shù)組,這里就不再介紹了

你總是可以將一個泛型類型轉(zhuǎn)化為原生類型。

具體的示例在上面我們講述與遺留代碼相互調(diào)用的時候已經(jīng)展示過了,相信你已經(jīng)能夠分析出為什么轉(zhuǎn)化為原生類型是不安全的。

泛型類是可以擴展或者實現(xiàn)其他的泛型類的。

就像我們上面一直寫的那樣,ArrayList<Banana> 是可以賦值給 List<Banana> 類型的引用的。

參考文章

  1. Java 核心技術(shù)卷1
  2. 胖胖在知乎的回答
  3. Java 協(xié)變與逆變
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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

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