協變與逆變1

在日常的開發中,你是否經常看見List<?>、List<T>、 List<Object>、List<? extends Number>、List<? super Integer>等形式的泛型定義。當你對這幾種類型不瞭解的時候也就無法理解逆變與協變。當然,逆變與協變的產生本質上還是由於Java的多態。

首先,來了解下以上講的幾種泛型。注意:本文用集合的泛型來解釋說明。
List<?>:表示存放一種未知的特定類型的集合。這種一般只能讀取數據,而不能寫入數據,可讀是因爲不管集合存放什麼類型的數據,該類一定是繼承自Object的,而不能寫是因爲集合存放的是特定的數據類型,但是編譯器又不知道具體的類型,因此無法向其寫入數據。比如

 

List<?> list = new ArrayList<Integer>();
list = new ArrayList<String>();
list.add(new Object())//編譯出錯,實際存放的可能是Number類型之類
Object o = list.get(0);//編譯正常

List<T>:表示存放一種已知的特定類型的集合。爲什麼說是已知,因爲在實際使用的時候,要將T替換成實際的類型,而不能像List<?>這樣直接使用,當然了,既然是確定的類型,就可進行讀寫。比如:

 

List<Integer> list1 = new ArrayList<>();
list1.add(1);
list1.get(0);

List<Object>:表示集合存放的是Object類型的數據,可對List<Object>進行讀寫,可能有人會將其與List<?>混淆了。我覺得可這樣理解,List<?>表示編譯器不知道存放的是什麼類型,因此可能是List<Integer>,也可能是List<String>,因此你往List<?>寫入int不合適,寫入String也不合適,寫入Object類型更不行(無法將父類的對象賦予子類的引用)。但是無論是Integer還是String,讀取出來的類型一定的Object類型的子類。因此可對List<?>讀取。而List<Object>,已經明確告訴編譯器List存放的Object類型的,因此可以向List<Object>寫入和讀取。

List<? extends Number>和List<? super Integer>的泛型類型就是本文要講的協變與逆變。在講這個概念前,再回憶下Java的多態,父類的引用可指向子類的對象。比如Fruit fruit = new Apple();注意:fruit的靜態類型是Fruit,實際類型是Apple。

那麼何爲協變與逆變?

假如現在有兩種類型:P和C,P是C的父類,根據多態可知P類型的引用可指向C類型的對象。此時用F(X)表示基於P和C的其他類型,如List<P>和List<C>。
協變:f(P)是f(C)的父類,f(p)的引用可指向f(C)的對象,此時稱爲協變。
逆變: f(P)是f(C)的子類,f(C)的引用可指向f(P)的對象,此時稱爲逆變。
不變: f(P)與f(C)不是父子關係。

在瞭解了逆變與協變的定義後,再用實際的例子來說明下。我們知道Number是Integer的父類型,但是List<Number>是List<Integer>的父類麼,答案明顯不是的。因爲List<Number> list = new ArrayList<Integer>()無法成立。也就是List<Number>與List<Integer>是不變的。爲什麼List<Number>引用無法指向List<Integer>對象?可以先假設爲可以,看以下代碼:

 

List<Number> list2 = new ArrayList<Integer>();//編譯出錯
list2.add(1.2); 
Integer number = (Integer) list2.get(0);

list2是List<Number>的引用,因此可以往list2添加任何Number類型及其子類的數據,這時我們加入一個double類型的。但是,又由於list2實際指向的是List<Integer>對象,因此從list2取出來的數據,根據泛型可知一定是Integer類型,因此對其強制類型轉換,這就與加入時double類型相矛盾了,因此編譯器也不允許我們這樣做。

下面就真正的解釋協變與逆變了。也就是本文一開始就提到的List<? extends Number>和List<? super Integer>。還是看下面例子:

 

List<? extends Number> list3 = new ArrayList<Integer>();//編譯通過
list3.add(1);//編譯報錯
Number number1 = list3.get(0);//編譯通過

通過上述例子可知,List<? extends Number>的引用可指向List<Integer>的對象,因此說明List<? extends Number>是協變的。但是隻能對協變的類型進行讀取而不能寫入。首先List<? extends Number>規定了集合類型的上界爲Number類型的,但是並沒有說明具體的類型,可能是Integer類型,也可能是Double類型,因此無法對其進行寫入。但是可以取是因爲,無論集合存放的是什麼類型,取出來的一定是Number類型的。有人可能會說,那我們不是把List<Integer>賦值給它了麼,爲什麼不能寫入Integer類型的數據?這邊在提醒下,在編譯期時檢查的是集合的靜態類型List<? extends Number>,而不是實際類型List<Integer>。再來看下另一個說明逆變的例子,如下:

 

List<? super Integer> list4 = new ArrayList<Number>();//編譯通過
list4.add(1);//編譯通過
Integer integer = list4.get(0);//編譯出錯

通過上述例子可知,List<? super Integer>的引用可指向List<Number>的對象,因此說明List<? super Integer>是逆變的。對逆變類型引用可進行數據寫入,但是讀取的時候,如果不進行強制類型轉換,編譯是無法通過的。首先List<? super Integer>規定了集合類型的下界爲Integer類型,而實際的類型爲List<Number>。因此我們在對集合進行數據寫入時,寫入了Integer類型,而實際存放的是Number類型的數據,根據多態可知此操作可行。但是進行讀取的時候,由於讀取的實際類型是Number類型,因此,不能將Number類型的數據賦值給Integer引用(子類的引用無法指向父類的對象)。

那麼何時使用協變與逆變呢?根據effective Java所寫的,當需要寫入數據時,使用逆變(? super形式);當需要讀取數據時,使用協變(? extends形式);當既要對數據進行讀取又要對數據進行寫入,使用不變(T)。一句話總結:協變是生產者,逆變是消費者。

PECS總結:

  • 要從泛型類取數據時,用extends;
  • 要往泛型類寫數據時,用super;
  • 既要取又要寫,就不用通配符(即extends與super都不用)。


作者:panmingjie
鏈接:https://www.jianshu.com/p/8ca2b685b44b
來源:簡書
著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。

發佈了15 篇原創文章 · 獲贊 25 · 訪問量 14萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章