Java泛型理解

寫在前面:安全

             泛型的目的就是爲了規範參數app

            1·泛型就是將類型參數化,其在編譯時才肯定具體的參數。學習

            2·泛型只存在於編譯階段,而不存在於運行階段ui

其實正常開發中不多用到下面的寫法,作一個瞭解就能夠了code


泛型除了咱們最基本的使用,還有更加複雜的應用,如:對象

List<? extends Number> list = new ArrayList();
List<? super Number> list = new ArrayList();

上面的 extends 和 super 關鍵字其實就是泛型的高級應用:泛型通配符。資源

但在講泛型通配符以前,咱們必須對編譯時類型和運行時類型有一個基本的瞭解,才能更好地理解通配符的使用。開發

編譯時類型和運行時類型get

咱們先來看看一個簡單的例子。it

Class Fruit{}
Class Apple extends Fruit{}

上面聲明一個 Fruit 類,Apple 類是 Fruit 類的子類。

接着下面咱們聲明一個蘋果對象:

Apple apple = new Apple();

這樣的聲明,我相信你們都沒有什麼異議,聲明一個 Apple 類型的變量指向一個 Apple 對象。在上面這段代碼中,apple 屬性指向的對象,其編譯時類型和運行時類型都是 Apple 類型。

但其實不少時候咱們也使用下面這種寫法:

Fruit apple = new Apple();

咱們使用 Fruit 類型的變量指向了一個 Apple 對象,這在 Java 的語法體系中也是沒有問題的。由於 Java 容許把一個子類對象(Apple對象)直接賦值給一個父類引用變量(Fruit類變量),通常咱們稱之爲「向上轉型」。

那問題來了,此時 apple 屬性所指向的對象,其編譯時類型和運行時類型是什麼呢?

不少人會說:apple 屬性指向的對象,其編譯時類型和運行時類型不都是 Apple 類型嗎?

正確答案是:apple 屬性所指向的對象,其在編譯時的類型就是 Fruit 類型,而在運行時的類型就是 Apple 類型。

這是爲何呢?

由於在編譯的時候,JVM 只知道 Fruit 類變量指向了一個對象,而且這個對象是 Fruit 的子類對象或自身對象,其具體的類型並不肯定,有多是 Apple 類型,也有多是 Orange 類型。而爲了安全方面的考慮,JVM 此時將 apple 屬性指向的對象定義爲 Fruit 類型。由於不管其是 Apple 類型仍是 Orange 類型,它們均可以安全轉爲 Fruit 類型。

而在運行時階段,JVM 經過初始化知道了它指向了一個 Apple 對象,因此其在運行時的類型就是 Apple 類型。

泛型中的向上轉型

當咱們明白了編譯時類型和運行時類型以後,咱們再來理解通配符的誕生就相對容易一些了。

仍是上面的場景,咱們有一個 Fruit 類,Apple 類是 Fruit 的子類。這時候,咱們增長一個簡單的容器:Plate 類。Plate 類定義了盤子一些最基本的動做:

public class Plate<T> {
    private List<T> list;
    public Plate(){} 
    public void add(T item){list.add(item);}
    public T get(){return list.get(0);}
}

按咱們以前對泛型的學習,咱們能夠知道上面的代碼定義了一個 Plate 類。Plate 類定義了一個 T 泛型類型,能夠接收任何類型。說人話就是:咱們定義了一個盤子類,這個盤子能夠裝任何類型的東西,好比裝水果、裝蔬菜。

若是咱們想要一個裝水果的盤子,那定義的代碼就是這樣的:

Plate<Fruit> plate = new Plate<Fruit>();

咱們直接定義了一個 Plate 對象,而且指定其泛型類型爲 Fruit 類。這樣咱們就能夠往裏面加水果了:

plate.add(new Fruit());
plate.add(new Apple());

按照 Java 向上轉型的原則,咱們固然也以爲 Java 泛型能夠向上轉型,即咱們上面關於水果盤子的定義能夠變爲這樣:

Plate<Fruit> plate = new Plate<Apple>();  //Error

但事實上,這種寫法是錯誤的,上面的代碼在編譯的時候會出現編譯錯誤。

按理說,這種寫法應該是沒有問題的,由於 Java 支持向上轉型嘛。

錯誤的緣由就是:泛型並不直接支持向上轉型,JVM 會要求其指向的對象是 Fruit 類型的對象。

正是爲了解決保持「向上轉型」概念在 Java 語言中的統一,使泛型也支持向上轉型,因此 Java 推出了通配符的概念。

上面這行代碼若是要正常編譯,只須要修改一下 Plate 類的聲明便可:

Plate<? extends Fruit> plate = new Plate<Apple>();

上面的這行代碼表示:plate 能夠指向任何 Fruit 類對象,或者任何 Fruit 的子類對象。Apple 是 Fruit 的子類,天然就能夠正常編譯了。

extends通配符的缺陷

雖然經過這種方式,Java 支持了 Java 泛型的向上轉型,可是這種方式是有缺陷的,那就是:其沒法向 Plate 中添加任何對象,只能從中讀取對象。

Plate<? extends Fruit> plate = new Plate<Apple>();
plate.add(new Apple()); //Compile Error
plate.get();    // Compile Success

能夠看到,當咱們嘗試往盤子中加入一個蘋果時,會發現編譯錯誤。可是咱們能夠從中取出東西。那爲何咱們會沒法往盤子中加東西呢?

這還得從咱們對盤子的定義提及。

Plate<? extends Fruit> plate = new Plate<XXX>();

上面咱們對盤子的定義中,plate 能夠指向任何 Fruit 類對象,或者任何 Fruit 的子類對象。也就是說,plate 屬性指向的對象其在運行時能夠是 Apple 類型,也能夠是 Orange 類型,也能夠是 Banana 類型,只要它是 Fruit 類,或任何 Fruit 的子類便可。即咱們下面幾種定義都是正確的:

Plate<? extends Fruit> plate = new Plate<Apple>();
Plate<? extends Fruit> plate = new Plate<Orange>();
Plate<? extends Fruit> plate = new Plate<Banana>();

這樣子的話,在咱們還未具體運行時,JVM 並不知道咱們要往盤子裏放的是什麼水果,究竟是蘋果,仍是橙子,仍是香蕉,徹底不知道。既然咱們不能肯定要往裏面放的類型,那 JVM 就乾脆什麼都不給放,避免出錯。

正是出於這種緣由,因此當使用 extends 通配符時,咱們沒法向其中添加任何東西。

那爲何又能夠取出數據呢?由於不管是取出蘋果,仍是橙子,仍是香蕉,咱們均可以經過向上轉型用 Fruit 類型的變量指向它,這在 Java 中都是容許的。

Fruit apple = plate.get();
Apple apple = plate.get();  //Error

能夠從上面的代碼看到,當你嘗試用一個 Apple 類型的變量指向一個從盤子裏取出的水果時,是會提示錯誤的。

因此當使用 extends 通配符時,咱們能夠取出全部東西。

總結一下,咱們經過 extends 關鍵字能夠實現向上轉型。可是咱們卻失去了部分的靈活性,即咱們不能往其中添加任何東西,只能取出東西。

super通配符的缺陷

與 extends 通配符類似的另外一個通配符是 super 通配符,其特性與 extends 徹底相反。

Plate<? super Apple> plate = new Plate<Fruit>();

上面這行代碼表示 plate 屬性能夠指向一個特定類型的 Plate 對象,只要這個特定類型是 Apple 或 Apple 的父類。也就是說,若是 EatThing 類是 Fruit 的父級,那麼下面的聲明也是正確的:

Plate<? super Apple> plate = new Plate<EatThing>();

固然了,下面的聲明確定也是對的,由於 Object 是任何一個類的父級。

Plate<? super Apple> plate = new Plate<Object>();

既然這樣,也就是說 plate 指向的具體類型能夠是任何 Apple 的父級,JVM 在編譯的時候確定沒法判斷具體是哪一個類型。但 JVM 能肯定的是,任何 Apple 的子類均可以轉爲 Apple 類型,但任何 Apple 的父類都沒法轉爲 Apple 類型。

因此對於使用了 super 通配符的狀況,咱們只能存入 T 類型及 T 類型的子類對象。

Plate<? super Apple> plate = new Plate<Fruit>();
plate.add(new Apple());
plate.add(new Fruit()); //Error

當咱們向 plate 存入 Apple 對象時,編譯正常。可是存入 Fruit 對象,就會報編譯錯誤。

而當咱們取出數據的時候,也是相似的道理。JVM 在編譯的時候知道,咱們具體的運行時類型能夠是任何 Apple 的父級,那麼爲了安全起見,咱們就用一個最頂層的父級來指向取出的數據,這樣就能夠避免發生強制類型轉換異常了。

Object object = plate.get();
Apple apple = plate.get();  //Error
Fruit fruit = plate.get();  //Error

從上面的代碼能夠知道,當使用 Apple 類型或 Fruit 類型的變量指向 plate 取出的對象,會出現編譯錯誤。而使用 Object 類型的額變量指向 plate 取出的對象,則能夠正常經過。

也就是說對於使用了 super 通配符的狀況,咱們取出的時候只能用 Object 類型的屬性指向取出的對象。

PECS原則

說到這裏,我相信你們已經明白了 extends 和 super 通配符的使用和限制了。咱們知道:

  • 對於 extends 通配符,咱們沒法向其中加入任何對象,可是咱們能夠進行正常的取出。
  • 對於 super 通配符,咱們能夠存入 T 類型對象或 T 類型的子類對象,可是咱們取出的時候只能用 Object 類變量指向取出的對象。

從上面的總結能夠看出,extends 通配符偏向於內容的獲取,而 super 通配符更偏向於內容的存入。咱們有一個 PECS 原則(Producer Extends Consumer Super)很好的解釋了這兩個通配符的使用場景。

Producer Extends 說的是當你的情景是生產者類型,須要獲取資源以供生產時,咱們建議使用 extends 通配符,由於使用了 extends 通配符的類型更適合獲取資源。

Consumer Super 說的是當你的場景是消費者類型,須要存入資源以供消費時,咱們建議使用 super 通配符,由於使用 super 通配符的類型更適合存入資源。

但若是你既想存入,又想取出,那麼你最好仍是不要使用 extends 或 super 通配符。

總結

Java 泛型通配符的出現是爲了使 Java 泛型也支持向上轉型,從而保持 Java 語言向上轉型概念的統一。但與此同時,也致使 Java 通配符出現了一些缺陷,使得其有特定的使用場景。

相關文章
相關標籤/搜索