這篇文章的誕生,其實來自於郭神給個人意見,一樣的也是對以前面試官問我:「泛型擦除是什麼,會帶來什麼問題?」一文中內容的一個補充,但願我若是有理解錯誤的地方能給我指正。java
從百度上只要這樣輸入關鍵詞,java 逆變與協變 你就能獲得相似與下方文字的統一解。面試
逆變與協變描述的是類型轉換後的繼承關係。數組
定義A,B兩個類型,A是由B派生出來的子類(A<=B),f()表示類型轉換如new List();安全
先來解釋一下,這幾句話都是什麼意思吧。bash
// 協變
List<? extends Fruit> fruit = new ArrayList<Apple>();
// 逆變
List<? super Apple> fruit = new ArrayList<Fruit>();
// 不變
List<Apple> fruit = new ArrayList<Apple>();
複製代碼
我想你必定會想問我,明明相似List<String> list = new ArrayList()
或者Fruit[] fruit = new Apple[10];
是能夠編譯成功的,可是不變的部分並不能編譯成功這是爲何呢?併發
其實List
內部並不自帶協變機制是有關係,也就是上面說的不變,而若是這樣寫List<Fruit> fruit = new ArrayList<Apple>();
就會編譯出錯的狀況。而逆變和協變的機制的完成,就須要使用到咱們以前在面試官問我:「泛型擦除是什麼,會帶來什麼問題?」已經有所涉及的extends
和super
來完成任務了。app
知道了上述的這些基本知識,咱們就來邏輯證實一下上面說的論據吧。函數
定義: A是B的子類,就能夠完成父類向子類的變換。post
這個知識點的內容,你必定程度上能夠參考向下轉型。學習
List list = new ArrayList();
List<Fruit> flist = new ArrayList<Apple>(); // 報錯
複製代碼
extends
做爲泛型中用於引入協變機制的關鍵詞,他的做用域咱們想來已經有所瞭解了,比較上述的兩端代碼,第一段是可以編譯成功的,第二段則是編譯失敗的,這是爲何呢?咱們已經說過了其實List
內部並不像自帶數組或者你直接建立一個對象同樣直接完成變化,相似於T
這樣的泛型通配符,它在編譯時並無對這樣的數據進行轉化。
其實我以前的文章中已經有所講述了,往前翻一下加深印象。
List<? extends Fruit> fruitList = new ArrayList<Apple>();
複製代碼
定義: A是B的子類,就能夠完成子類向父類的變換。(必定程度上可參考向上轉型)
List list =(List) new ArrayList();
List<Fruit> flist = (ArrayList<Fruit>)new ArrayList<Apple>(); // 依舊編譯錯誤
複製代碼
同理爲了解決這樣的問題,Java
引入了super
關鍵詞
List<? super Apple> fruit1 = new ArrayList<Fruit>();
複製代碼
什麼是不變,我就不說了。想來讀者從上面的內容看完以後,已經知道了不變這個概念大概對應的含義了,其實就是下面這段代碼,只能等於自己。
List<Apple> list = new ArrayList<Apple>();
複製代碼
來了來了,又是一個重複的知識點。其實轉化一下問題就是爲何要引入逆變與協變這兩個機制呢?
先來想一下,泛型在運行時有什麼問題? 很顯然,泛型擦除嘛!!
那泛型擦除的具體表現是什麼? 看過以前文章的讀者確定想罵我了,不就是變成了Object
,最後經過強轉再把類型轉化回來嘛。
沒錯了,那咱們的從新溫習一下ArrayList
的get()
源碼先。
public E get(int index) {
rangeCheck(index);
checkForComodification();
return ArrayList.this.elementData(offset + index); // 1 -->
}
E elementData(int index) {
return (E) elementData[index];
}
複製代碼
這個總體部分一共調用了兩個函數,而第二個函數,你清晰可見的進行了強制轉化,爲何?由於他保存的是Object
,而不是咱們賦予的Apple
啊。
經過引入協變的機制,List<? extends Fruit> fruits = new ArrayList<Apple>();
意味着某個繼承自Fruit的具體類型,知道了上界,可是下界處於一個徹底未知的狀態下了。在這樣的狀況下,最明顯的狀況就是你沒法完成咱們的插入操做了。
爲何會這樣呢?其實很簡單,由於你忘記本身是誰了。你只知道上界,其實這跟我之前一直好奇的地方有類似,就像是族譜,最頂上的人是你的曾曾曾。。。。。祖父和祖母,而咱們只是族譜最下面的那幾個毛頭娃兒,忘記了本身是誰,你會知道你上面的那些長輩是誰嗎?
List<? extends Fruit> fruits = new ArrayList<Apple>();
fruits.add(new Apple()); // 編譯錯誤
fruits.add(new Fruit()); // 編譯錯誤
fruits.add(new Object()); // 編譯錯誤
複製代碼
而逆變機制就不同了,他已經知道了下界,可是殊不知道上界,那這個時候他的子子孫孫他就認識了,可是長輩那一欄,就像是被撕掉了同樣,他不知道該去哪裏找他們;或者說咱們知道他們,可是他們有不少東西咱們繼承並發展了新的東西,與他們的匹配度再也不相同,就不能讓他們加入咱們的行列了,而子子孫孫拿走了你的所有,而且有着本身的新玩意兒(固然這些新玩意兒再也不咱們的考慮範圍內了),因此你能看到這樣的狀況。
List<? super Apple> apples = new ArrayList<Fruit>();
apples.add(new Apple());
apples.add(new Jonathan());
apples.add(new Fruit()); // 編譯錯誤
複製代碼
List<? extends Fruit> fruits = new ArrayList<Apple>();
Apple apple = fruits.get(0);
Jonathan jonathan = fruits.get(0); // 編譯錯誤
Fruit fruit = fruits.get(0);
複製代碼
對於extends
可以得到比大於或者等於他自己的數據,這是爲何?咱們也說過了,他肯定了上界,若是經過子類那數據,就會出現數據不匹配的狀況。因此天然而然的對這方面進行了限制。就好比說fruits.get(0)
獲取的實際上是Apple
的裏一個子類B
,那這個時候,咱們假設第三行代碼運行成功,那麼數據就會出現不匹配,由於你沒法保證Jonathan
的數據和B
徹底保持一致,可是他們倆的數據必定和Apple
保持一致。
List<? super Apple> apples = new ArrayList<Fruit>();
Object jonathan = apples.get(0);
// 其他只能經過強制轉化得到
複製代碼
而逆變的數據獲取中,數據信息已經所有丟失,因此再也不適合獲取操做。
在《Effective Java》給出過一個十分精煉的回答:producer-extends, consumer-super(PECS)。
從字面意思理解就是,extends是限制數據來源的(生產者),而super是限制數據流入的(消費者)。而顯然咱們一些常用到的代碼中也都是符合了這一規則。
public static <T> T min(Collection<? extends T> coll, Comparator<? super T> comp) {
if (comp==null)
return (T)min((Collection) coll);
Iterator<? extends T> i = coll.iterator();
T candidate = i.next();
while (i.hasNext()) {
T next = i.next();
if (comp.compare(next, candidate) < 0)
candidate = next;
}
return candidate;
}
複製代碼
Collections
中的一個min()
函數,從字面意思咱們應該就能知道把,就是獲取最小值的意思了,你看看他放入了什麼玩意兒Collection<? extends T> coll, Comparator<? super T> comp
,而coll
的數據相似就是協變,一個適合獲取的方案。而比較器comp
使用的就是就是super
,由於要進行存儲之類的操做。
上面這樣作法的一切緣由是其實都是爲了數據安全。
已知數據下界,應該細化存儲;已知數據上界,應該粗糙拿取。(其中細化就是指用子類或者自己存,粗糙就是用父類或者自己取)
以上就是個人學習成果,若是有什麼我沒有思考到的地方或是文章內存在錯誤,歡迎與我分享。
相關文章推薦: