java协变 生产者理解_Java进阶知识点:协变与逆变
一、背景
要搞懂Java中的協(xié)辦與逆變,不得不從繼承說(shuō)起,如果沒(méi)有繼承,協(xié)變與逆變也天然不存在了。
我們知道,在Java的世界中,存在繼承機(jī)制。比如MochaCoffee類(lèi)是Coffee類(lèi)的派生類(lèi),那么我們可以在任何時(shí)候使用MochaCoffee類(lèi)的引用去替換Coffee類(lèi)的引用(重寫(xiě)函數(shù)時(shí),形參必須與重寫(xiě)函數(shù)完全一致,這是一處列外),而不會(huì)引發(fā)編譯錯(cuò)誤(至于會(huì)不會(huì)引發(fā)程序功能錯(cuò)誤,取決于代碼是否符合里氏替換原則)。
簡(jiǎn)而言之,如果B類(lèi)是A類(lèi)的派生類(lèi),那么B類(lèi)的引用可以賦值給A類(lèi)的引用。
賦值的方式最常見(jiàn)有兩種。
第一:使用等于運(yùn)算符顯式賦值
Coffee coffee = new MochaCoffee();
上述代碼可以分兩階段理解,首先new MochaCoffee()返回MochaCoffee的引用,然后將此引用顯式賦值給Coffee類(lèi)型的引用。
第二:函數(shù)傳參賦值
public class Main {
public static void main(String[] args) {
function(new MochaCoffee());
}
public static void function(Coffee coffee) {
}
}
基礎(chǔ)知識(shí)復(fù)習(xí)完后,我們正式開(kāi)始進(jìn)入?yún)f(xié)變與逆變的世界,首先我們來(lái)看如下常見(jiàn)代碼:
Coffee a[] = new MochaCoffee[10];
List extends Coffee> b = new ArrayList();
List super MochaCoffee> c = new ArrayList();
這三行代碼每一行單獨(dú)看,好像都可以勉強(qiáng)看得懂,但是這三行代碼似乎透露出一些讓人內(nèi)心秩序隱隱不安的疑惑:
MochaCoffee[]是Coffee[]的子類(lèi)?
ArrayList是List extends Coffee>的子類(lèi)?
ArrayList是List super MochaCoffee>的子類(lèi)?
我們只學(xué)習(xí)過(guò)Class之間有繼承關(guān)系,這些數(shù)組、容器類(lèi)型之間難道也有繼承關(guān)系,這種繼承關(guān)系在JDK哪一處源碼中有定義?還有沒(méi)有其他類(lèi)似的情況?
如果你也有類(lèi)似的問(wèn)題,說(shuō)明你的知識(shí)體系中缺失了一個(gè)知識(shí)點(diǎn),這就是我們今天講的Java中的協(xié)變與逆變。
二、逆變與協(xié)變
2.1 定義
假設(shè)F(X)代表Java中的一種代碼模式,其中X為此模式中可變的部分。如果B是A的派生類(lèi),而F(B)也享受F(A)派生類(lèi)的待遇,那么F模式是協(xié)變的,如果F(A)反過(guò)來(lái)享受F(B)派生類(lèi)的待遇,那么F模式是逆變的。如果F(A)和F(B)之間不享受任何繼承待遇,那么F模式是不變的。(這里的繼承待遇指的是前面復(fù)習(xí)到的“如果B類(lèi)是A類(lèi)的派生類(lèi),那么B類(lèi)的引用可以賦值給A類(lèi)的引用。”)
Java中絕大部分代碼模式都是不變的(大家可以安心了)。
2.2 Java中的協(xié)變與協(xié)變模式
Java中目前已知的支持協(xié)變與逆變的模式,我總結(jié)了三類(lèi),歡迎大家補(bǔ)充。
2.2.1 F(X) = 將X數(shù)組化,此時(shí)F模式是協(xié)變的
Coffee a[] = new Coffee[10];
MochaCoffee b[] = new MochaCoffee[10];
a = b; //b可以賦值給a
這可以回答之前的問(wèn)題,雖然MochaCoffee[]不是Coffee[]的子類(lèi),但數(shù)組化這種代碼模式是協(xié)變的,所以MochaCoffee[]也可以直接賦值給Coffee[]。
值得注意的是,雖然數(shù)組是協(xié)變的,但是數(shù)組是會(huì)記住實(shí)際類(lèi)型并在每一次往數(shù)組中添加元素時(shí)做類(lèi)型檢查。比如如下代碼雖然可以利用數(shù)組的協(xié)變性通過(guò)編譯,但是運(yùn)行時(shí)依然會(huì)拋出異常。
Coffee a[] = new MochaCoffee[10];
a[0] = new Coffee(); //拋出ArrayStoreException
這也是數(shù)組的協(xié)變?cè)O(shè)計(jì)被廣為詬病的原因,因?yàn)楫惓?yīng)該盡量在編譯時(shí)就發(fā)現(xiàn),而不是推遲到運(yùn)行時(shí)。不過(guò)數(shù)組支持協(xié)變后,java.util.Arrays#equals(java.lang.Object[], java.lang.Object[])這種類(lèi)型的函數(shù)就不需要為每種可能的數(shù)組類(lèi)型去分別實(shí)現(xiàn)一次了。數(shù)組的協(xié)變?cè)O(shè)計(jì)有歷史版本兼容性方面的考慮等,Java的每一個(gè)設(shè)計(jì)可能不是最優(yōu)的,但確實(shí)是設(shè)計(jì)者在當(dāng)時(shí)的情況下可以做出的最好選擇。
2.2.2 F(X) = 將X通過(guò) extend X>語(yǔ)法作為泛型參數(shù),此時(shí)F模式是協(xié)變的
List extends Coffee> a = new ArrayList();
List extends MochaCoffee> b = new ArrayList();
a = b; //b可以賦值給a
同樣的,雖然ArrayList不是List extends Coffee>的子類(lèi),但是List extends X>這種代碼模式是協(xié)變的,所以b可以直接賦值給a。
值得注意的是,雖然利用協(xié)變性,可以將ArrayList賦值給List extends Coffee>,但是賦值后,List extends Coffee>中不能取出MochaCoffee,同時(shí)也只能添加null。因?yàn)長(zhǎng)ist跟數(shù)組不一樣,它在運(yùn)行時(shí)插入元素時(shí),類(lèi)型信息已經(jīng)被擦除為Object,無(wú)法做類(lèi)型檢測(cè),只能依靠聲明在編譯時(shí)做嚴(yán)格的類(lèi)型檢查,List extends?Coffee>聲明意味著這個(gè)容器中的元素類(lèi)型不確定,可能是Coffee的任何子類(lèi),所以往里面添加任何類(lèi)型都是不安全的,但是可以取出Coffee類(lèi)型。如下:
List extends Coffee> a = new ArrayList();
//a.add(new MochaCoffee()); //不能添加MochaCoffee
//a.add(new Coffee()); //也不能添加Coffee
a.add(null); //可以添加null
Coffee coffee = a.get(0); //可以取出Coffee
2.2.3?F(X) = 將X通過(guò) super?X>語(yǔ)法作為泛型參數(shù),此時(shí)F模式是逆變的
List super MochaCoffee> a = new ArrayList();
List super Coffee> b = new ArrayList();
a = b; //b可以賦值給a
ArrayList不是List super MochaCoffee>的子類(lèi),但是List super X>這種代碼模式是逆變的,所以b可以直接賦值給a。
值得注意的是,雖然利用逆變性,可以將ArrayList賦值給List super MochaCoffee>,但是賦值后,List super MochaCoffee>中不能添加Coffee,同時(shí)也只能取出Object(除非進(jìn)行強(qiáng)制類(lèi)型轉(zhuǎn)換)。List super MochaCoffee>聲明意味著這個(gè)容器中的元素類(lèi)型不確定,可能是MochaCoffee的任何基類(lèi),所以往里面添加MochaCoffee及其子類(lèi)是安全的,但是取出的類(lèi)型就只能是最頂層基類(lèi)Object了。如下:
List super MochaCoffee> a = new ArrayList();
// a.add(new Coffee()); //不能添加Coffee
a.add(new MochaCoffee()); //可以添加MochaCoffee
Object object = a.get(0); //只能取出Object
注:沒(méi)有extend和super關(guān)鍵字加持的泛型模式都是不變的,A與B之間有繼承關(guān)系,但是List和List之間不享受任何繼承待遇,這就解決了上面提到數(shù)組協(xié)變導(dǎo)致的問(wèn)題,讓類(lèi)型錯(cuò)誤在編譯時(shí)就可以被發(fā)現(xiàn)。
2.3 PECS原則
2.2.2和2.2.3中的注意事項(xiàng),也體現(xiàn)了著名的PECS原則:“Producer Extends,Consumer Super”。
因?yàn)槭褂?extends T>后,如果泛型參數(shù)作為返回值,用T接收一定是安全的,也就是說(shuō)使用這個(gè)函數(shù)的人可以知道你生產(chǎn)了什么東西;
而使用 super T>后,如果泛型參數(shù)作為入?yún)?#xff0c;傳遞T及其子類(lèi)一定是安全的,也就是說(shuō)使用這個(gè)函數(shù)的人可以知道你需要什么東西來(lái)進(jìn)行消費(fèi)。
比如Java8新增的函數(shù)接口java.util.function.Consumer#andThen方法就體現(xiàn)了Consumer Super這一原則。
三、總結(jié)
1、數(shù)組是協(xié)變的。
2、extend關(guān)鍵字加持的泛型是協(xié)變的。
3、super關(guān)鍵字加持的泛型是逆變的。
4、注意數(shù)組和泛型容器中添加和獲取元素的類(lèi)型限制。
《新程序員》:云原生和全面數(shù)字化實(shí)踐50位技術(shù)專(zhuān)家共同創(chuàng)作,文字、視頻、音頻交互閱讀總結(jié)
以上是生活随笔為你收集整理的java协变 生产者理解_Java进阶知识点:协变与逆变的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 非自然人投资或控股是什么意思
- 下一篇: 开发c s架构java应用程序6_jav