List<? extends T>與List<? super T>的差別
這個問題,初級工程師在做面試題時會出現。進階工程師很少會遇到,但實際上,不少工作了 5 年左右的工程師也回答的不是很好。基于此,整理了本文,分享給大家!
我們先來看一下名詞解釋:
(1)?
?表示類型通配符,即具體傳什麼參數類型,在List定義時不用考慮。
(2) <T>
<T>
這裡的 <> 表示泛型,T 表示泛型中裝載的類型為T類型,等到需要的時候,我們可以具體這個 T。我們在使用動态數組實作 ArrayList 的時候,如果希望這個 ArrayList 不僅僅支援一個類型的話,我們可以給這個 ArrayList 定義泛型,泛型中存放的是T類型。在實際建立出這個 ArrayList 對象時,我們可以指定泛型中的具體類型。
(3) <? extends T>
<? extends T>
類型上界,這裡的 ? 可以是 T 類型或者 T 的子類類型。
(4) <? super T>
<? super T>
類型下界,這裡的?可以是T類型或者T的超類類型,但不代表我們可以往裡面添加任意超類類型的元素。
在List中引入通配符界限限制的假設
不管是
List<? extends T>
還是
List<? super T>
,如果能讀取元素,那麼這個元素一定能轉化為 T 類型,注意不是強制類型轉換,強制類型轉換是容易出現問題。
顯然
List<? extends T>
内都是 T 的子類類型,能夠向上轉型為 T 類型,是以該 list 可以讀取。
而
List<? super T>
内可以是 T 的超類類型,T 的超類轉 T 是有可能出現異常的。
那我幹脆轉化成 Object 類型不好嗎,所有類的基類都是 Object,不屬于強制類型轉換。哥們,轉換成 Object 了,那你還圖個啥?轉換為 Object 類型是沒有意義的。
假設
List<? extends T>
能添加元素,那麼需要滿足添加的任意元素需要能夠直接轉化成 T 的任何一個子類,T 的子類 A 和子類 B 是不能互相轉化的,顯然該 list 是不能添加元素的。
假設
List<? super T>
能添加元素,那麼同樣需要滿足添加的任意元素能夠直接轉化成 T 的任何一個超類。此時添加 T 的子類元素就能滿足該要求,因為 T 的任意子類可以向上轉型成 T 的任何超類。
List<? extends T>
List<? extends T>
是被設計用來讀取資料的泛型,并且隻能讀取類型為 T 的元素。原因如下:
元素是可以進行向上轉型的,是以,我們可以這樣做來讀取元素。
List<? extends Number> list = new ArrayList<>();
Number number = list.get(0);
可以讀取,但不能寫入,比如以下的代碼就直接報錯。
public class Main {
static class A { }
static class B extends A { }
static class C extends A { }
public static void main(String[] args){
List<? extends A> list = new ArrayList<>();
list.add(new A());//編譯報錯
list.add(new B());//編譯報錯
list.add(new C());//編譯報錯
}
}
A 的子類 B 與子類 C 是不能互相轉換的,是以是不能往該 list 中添加元素。
雖然不能添加元素,但可以在初始化的時候,接受一個已經定義好的 list,而該 list 存放的類型一定相同。是以,
List<? extends T>
可直接接受一個定義好的 list。
public static List<Integer> getList(){
List<Integer> list=new ArrayList<>();
list.add(1);
return list;
}
// ....
public static void main(String[] args){
List<? extends Number> list = new ArrayList<>();
list=getList();
}
List<? super T>
List<? super T>
是被設計用來添加資料的泛型,并且隻能添加 T 類型或其子類類型的元素。
為什麼隻能是 T 類型及其子類型元素,超類類型的元素不可以嗎?
超類類型轉化為 T 類型,是需要強制類型轉換的,是容易出現異常的,無法保障的。
而傳入 T 類型及其子類類型時,能夠直接轉化為 T 的任意超類類型。比如,下面的代碼是可以運作的
public class Main {
static class A { }
static class B extends A { }
static class C extends A { }
public static void main(String[] args){
List<? super A> list = new ArrayList<>();
list.add(new A());
list.add(new B());
list.add(new C());
}
}
該 list 也可以讀取其中的元素,從第二節可以得出,隻能用 Object 接收,沒多大意義。
List<? super Integer> list2 = new ArrayList<>();
list2.add(new Integer(1));
Object integer=list2.get(0);
如果我們使用 Object 類型來接收擷取到的元素,那麼元素本身的類型就會丢失,是以,我們不使用
List<? super T>
來擷取元素。
如果我們非要使用
List<? super Integer>
中的 Integer 類型來接收擷取到的元素,那麼必須進行強制類型轉換,是會出現異常的,無法保障。
List<? super Integer> list2 = new ArrayList<>();
list2.add(new Integer(1));
Integer integer1= (Integer) list2.get(0);
總結
(1)
List<? extends T>
适用于讀取資料,讀取出來的資料全部用T類型接收。如果我們往此 list 中添加 T 類型不同的子類的話,各種子類無法互相轉換,是以不能添加元素,但可接受初始指派。
(2)
List<? super T>
适用于添加元素,隻能添加 T 類型或其子類類型。因為這些類型都能轉換為T的任意超類類型(向上轉型),是以我們可以對此 list 添加元素。隻能用 Object 類型來接收擷取到的元素,但是這些元素原本的類型會丢失。
更加通俗易懂的例子
什麼,你還沒明白?那我舉一些直覺的例子。
注意:
向上轉型是安全的,向下轉型是不安全的,除非你知道 List 中的真實類型,否則向下轉型就會報錯
。
extends
List<? extends Number> foo3
意味着下面的指派語句都是合法的:
List<? extends Number> foo3 = new ArrayList<Number>(); // Number "extends" Number (in this context)
List<? extends Number> foo3 = new ArrayList<Integer>(); // Integer extends Number
List<? extends Number> foo3 = new ArrayList<Double>(); // Double extends Number
- 讀取
給定上述可能的指派語句,能保證你從
List foo3
中取出什麼樣類型的對象?
- 你可以讀取一個
對象,因為上面任意一個list都包含Number
對象或者Number
子類的對象(上面的Number、Integer、Double都可以轉型成Number,并且是安全的,是以讀取總是可以的)。如下代碼就不會報錯:Number
List<? extends Number> foo4 = new ArrayList<Integer>();
Number number = foo4.get(0);
- 你不能讀取一個
對象,因為Integer
可能指向的是foo3
(與其運作時發現Double轉成Integer報錯,不如編譯時就不讓從List<Double>
中取foo3
對象)。如下代碼編譯時會報Integer
錯的:Incompatible types
List<? extends Number> foo4 = new ArrayList<Integer>();
Integer number = foo4.get(0);
因為編譯的時候編譯器隻知道foo4引用是一個List<? extends Number>,要到運作時才會綁定到new ArrayList(),是以編譯的時候是無法判斷foo4指向的List中到底是什麼類型,唯一能确定的就是這個類型是Number的子類(或者就是Number類)。
- 你也不能讀取一個
對象,因為Double
可能指向的是foo3
。List<Integer>
- 寫入
給定上述可能的指派語句,你能往
List foo3
中添加什麼類型的對象進而保證它對于所有可能的
ArrayList
都是合法的呢?
- 你不能添加一個
對象,因為Integer
可能指向的是foo3
。如下代碼是會編譯報錯的:List<Double>
List<? extends Number> foo4 = new ArrayList<Integer>();
foo4.add(new Integer(1));
因為編譯期間是無法知道foo4指向的ArrayList中到底放的是什麼類型,隻有到運作時才知道(就是Java所謂的晚綁定或運作時綁定)。與其到運作時發現往一個ArrayList中add一個Integer導緻抛出類型轉換異常,倒不如編譯時就報錯,即使ArrayList中放的就是Integer類型。
- 你不能添加一個
對象,因為Double
可能指向的是foo3
。List<Integer>
- 你不能添加一個
對象,因為Number
可能指向的是foo3
。List<Integer>
「總結一下」:你不能往
List<? extends T>
中添加任何對象,因為你不能保證
List
真正指向哪個類型,是以不能确定添加的對象就是
List
所能接受的類型。能保證的,僅僅是你可以從
List
中讀取的時候,你獲得的肯定是一個
T
類型的對象(即使是
T
類型的子類對象也是
T
類型的)。
super
現在考慮
List<? super T>
包含通配符的聲明
List<? super Integer> foo3
意味着下面任何一個指派語句都是合法的:
List<? super Integer> foo3 = new ArrayList<Integer>(); // Integer is a "superclass" of Integer (in this context)
List<? super Integer> foo3 = new ArrayList<Number>(); // Number is a superclass of Integer
List<? super Integer> foo3 = new ArrayList<Object>(); // Object is a superclass of Integer
- 讀取
- 你不能保證是一個
對象,因為Integer
可能指向一個foo3
或者List<Number>
。List<Object>
- 你不能保證是一個
對象,因為Number
可能指向一個foo3
。List<Object>
- 你能保證的僅僅是它一定是一個
類的執行個體或者Object
子類的執行個體(但是你不知道到底是哪個子類)。Object
- 寫入
- 你可以添加一個
執行個體,因為Integer
類型對于上述所有的list都是合法的。Integer
- 你可以添加任何
子類的執行個體,因為一個Integer
子類的執行個體都可以向上轉型成上面清單中的元素類型。Integer
- 你不可以添加
類型,因為Double
可能指向的是foo3
。ArrayList<Integer>
- 你不可以添加
類型,因為Number
可能指向的是foo3
。ArrayList<Integer>
- 你不可以添加
類型,因為Object
可能指向的是foo3
。ArrayList<Integer>
PECS
- "Producer Extends"的意思是,如果你需要一個
去生産List
類型values(也就是說你需要去list中讀取T
類型執行個體),你需要聲明這個T
中的元素為List
,例如? extends T
,但是你不能往裡面添加元素。List<? extends Integer>
- "Consumer Super"的意思是,如果你需要一個
去消費List
類型values(也就是說你需要往list中添加T
類型執行個體),你需要聲明這個T
中的元素為List
,例如? super T
。但是不能保證你從這個list中讀取出來對象類型。List<? super Integer>
- 如果你既需要往list中寫,也需要從list中讀,那麼你就不能用通配符
,必須用精确的類型,比如?
。List<Integer>