1. 程式人生 > >你了解泛型通配符與上下界嗎?

你了解泛型通配符與上下界嗎?

最小 public 對象 ogr 控制 water 基礎上 pri 賦值

在進入主題之前, 我們先簡單說一下 Java 的泛型(generics)。它是JDK 5中引入的一個新特性,允許在定義類和接口的時候使用類型參數(type parameter)。聲明的類型參數在使用時用具體的類型來替換。泛型最主要的應用是在JDK 5中的新集合類框架中。

今天我們主要說如下內容:

  • 泛型的背景

  • 通配符以及上下界

  • 泛型及通配符的使用場景

為什麽使用泛型及背後的問題?
我們來看一下官方的說法:

Stronger type checks at compile time.A Java compiler applies strong type checking to generic code and issues errors if the code violates type safety. Fixing compile-time errors is easier than fixing runtime errors, which can be difficult to find.

Elimination of casts.

Enabling programmers to implement generic algorithms.By using generics, programmers can implement generic algorithms that work on collections of different types, can be customized, and are type safe and easier to read.

是的, 終止目的就是想把程序員解放出來,關註他們更應該關註的事情上面去。當我第一次學習 Java 的泛型時,總感覺它類似於 C++ 中的模板。但隨著慢慢的深入了解發現它們之間有本質的區別。

Java 中的泛型基本上完全在編譯器中實現,由編譯器執行類型檢查和類型推斷,然後生成普通的非泛型的字節碼。這種實現技術稱為 擦除(erasure)(編譯器使用泛型類型信息保證類型安全,然後在生成字節碼之前將其清除),這項技術有一些奇怪,並且有時會帶來一些令人迷惑的後果。

對於泛型概念的引入,開發社區的觀點是褒貶不一。從好的方面來說,上面已經說了,主要是在編譯時刻就能發現很多明顯的錯誤。而從不好的地方來說,主要是為了保證與舊有版本的兼容性,Java 泛型的實現上存在著一些不夠優雅的地方。

下面我們來看一下,泛型類型的一個定義,後面我們要在這個的基礎上進行改造
public class Box<T> {

// T stands for "Type"

private T t;

public Box(T t) ? ?{ this.t = t;? }?

public void set(T t) { this.t = t; }

public T get() { return t; }

}:

接下來下面我們來聊聊 Java 泛型的通配符, 記得剛開始看到通配符(?)時我是驚喜的,因為既然有通配符那麽就可以這樣定義:

public void doSometing(List<?> list) {

list.add(1); //illegal

}

可是我們如上寫法,總是出現編譯錯誤,然後從驚喜變成驚嚇,心想有什麽卵用了。最後發現原因是在於通配符的表示的類型是未知的。那在這種情況下,我們可以使用上下界來限制未知類型的範圍。好吧,寫了那麽多, 終於等到今天的主角登場了,容易嗎?

還記得我們上面定義的 Box 嗎, 現在我們再定義 Fruit 類以及它的子類 Orange 類。

class Fruit { }

class Orange extends Fruit {}

現在我們想它裏面能裝水果,那麽我可以這麽寫。
Box<Fruit> box = Box<Orange>(new Orange) //illegal

不幸的是編譯器會報錯,這就尷尬了,why?why? why?實際上,編譯器認為的容器之間沒有繼承關系。所以我們不能這樣做。

為了解決這樣的問題, 大神們想出來了<? extens T> 和 <? super T> 的辦法,來讓它們之間發生關系。
上界通配符(Upper Bounded Wildcards)
現在我們把上面的 Box 定義改成:
Box<? extends Fruit>

這就是上界通配符, 這樣 Box 及它的子類如 Box 就可以賦值了。
Box<? extends Fruit> box = new Box<Orange>(new Orange)

當我們擴展一下上面的類, 食物分成為水果和蔬菜類, 水果有蘋果和橘子。
在上面的結構中, Box<? extends Fruit> 涵蓋下面的藍色的區域。
技術分享圖片
上界只能外圍取,不能往裏放
我們先看一下下面的例子:

Box<? extends Fruit> box = new Box<Orange>(new Orange);

//不能存入任何元素

box.set(new Fruit); //illegal

box.set(new Orange);//illegal

//取出來的東西只能存放在Fruit或它的基類裏

Fruit fruit = box.get();

Object fruit1 = box.get();

Orange fruit2 = box.get(); //illegal

上面的註釋已經很清楚了, 往 Box 裏放東西的 set() 方法失效, 但是 get() 方法有效。

原因是 Java 編譯器只知道容器內是 Fruit 或者它的派生類, 但是不知道是什麽類型。可能是 Fruit、 可能是 Orange、可能是Apple?當編譯器在看到 box 用 Box 賦值後, 它就把容器裏表上占位符 “AAA” 而不是 “水果”等,當在插入時編譯器不能匹配到這個占位符,所有就會出錯。
下界通配符(Lower Bounded Wildcards)
和上界相對的就是下界 ,語法表示為:
<? super T>

表達的相反的概率:一個能放水果及一切水果基類的 Box。 對應上界的那種圖, 下圖 Box<? super Fruit> 覆蓋×××區域。
技術分享圖片

下界不影響往裏存,但往外取只能放在Object 對象裏
同上界的規則相反,下界不影響往裏存,但往外取只能放在Object 對象裏。

因為下界規定元素的最小的粒度,實際上是容器的元素的類型控制。所以放比 Fruit 粒度小的如 Orange、Apple 都行, 但往外取時, 只有所有類的基類Object對象才能裝下。但是這樣的話,元素的類型信息就全部消失了。

使用場景
在使用泛型的時候可以遵循一些基本的原則,從而避免一些常見的問題。

在代碼中避免泛型類和原始類型的混用。比如List

和List不應該共同使用。這樣會產生一些編譯器警告和潛在的運行時異常。當需要利用JDK 5之前開發的遺留代碼,而不得不這麽做時,也盡可能的隔離相關的代碼。
在使用帶通配符的泛型類的時候,需要明確通配符所代表的一組類型的概念。由於具體的類型是未知的,很多操作是不允許的。

泛型類最好不要同數組一塊使用。你只能創建new List<?>[10]這樣的數組,無法創建new List[10]這樣的。這限制了數組的使用能力,而且會帶來很多費解的問題。因此,當需要類似數組的功能時候,使用集合類即可。

不要忽視編譯器給出的警告信息。

PECS 原則
如果要從集合中讀取類型T的數據, 並且不能寫入,可以使用 上界通配符(<?extends>)—Producer Extends。

如果要從集合中寫入類型T 的數據, 並且不需要讀取,可以使用下界通配符(<? super>)—Consumer Super。

如果既要存又要取, 那麽就要使用任何通配符。

你了解泛型通配符與上下界嗎?