Kotlin與Java 泛型缺陷和應(yīng)用場(chǎng)景詳解
引言
全文分為 視頻版 和 文字版
- 文字版: 文字側(cè)重細(xì)節(jié)和深度,有些知識(shí)點(diǎn),視頻不好表達(dá),文字描述的更加準(zhǔn)確
- 視頻版: 視頻會(huì)更加的直觀,看完文字版,在看視頻,知識(shí)點(diǎn)會(huì)更加清楚
視頻版 bilibili 地址:https://b23.tv/AdLtUGf
泛型對(duì)于每個(gè)開(kāi)發(fā)者而言并不陌生,平時(shí)在項(xiàng)目中會(huì)經(jīng)常見(jiàn)到,但是有很多小伙伴們,每次見(jiàn)到通配符 ? extends 、 ? super 、 out 、 in 都傻傻分不清楚它們的區(qū)別,以及在什么情況下使用。
通過(guò)這篇文章將會(huì)學(xué)習(xí)的到以下內(nèi)容。
- 為什么要有泛型
- Kotlin 和 Java 的協(xié)變
- Kotlin 和 Java 的逆變
- 通配符
? extends、? super、out、in的區(qū)別和應(yīng)用場(chǎng)景 - Kotlin 和 Java 數(shù)組協(xié)變的不同之處
- 數(shù)組協(xié)變的缺陷
- 協(xié)變和逆變的應(yīng)用場(chǎng)景
為什么要有泛型
在 Java 和 Kotlin 中我們常用集合( List 、 Set 、 Map 等等)來(lái)存儲(chǔ)數(shù)據(jù),而在集合中可能存儲(chǔ)各種類(lèi)型的數(shù)據(jù),現(xiàn)在我們有四種數(shù)據(jù)類(lèi)型 Int 、 Float 、 Double 、 Number,假設(shè)沒(méi)有泛型,我們需要?jiǎng)?chuàng)建四個(gè)集合類(lèi)來(lái)存儲(chǔ)對(duì)應(yīng)的數(shù)據(jù)。
class IntList{ }
class Floatlist{}
class DoubleList{}
class NumberList{}
......
更多
如果有更多的類(lèi)型,就需要?jiǎng)?chuàng)建更多的集合類(lèi)來(lái)保存對(duì)應(yīng)的數(shù)據(jù),這顯示是不可能的,而泛型是一個(gè) "萬(wàn)能的類(lèi)型匹配器",同時(shí)有能讓編譯器保證類(lèi)型安全。
泛型將具體的類(lèi)型( Int 、 Float 、 Double 等等)聲明的時(shí)候使用符號(hào)來(lái)代替,使用的時(shí)候,才指定具體的類(lèi)型。
// 聲明的時(shí)候使用符號(hào)來(lái)代替
class List<E>{
}
// 在 Kotlin 中使用,指定具體的類(lèi)型
val data1: List<Int> = List()
val data2: List<Float> = List()
// 在 Java 中使用,指定具體的類(lèi)型
List<Integer> data1 = new List();
List<Float> data2 = new List();
泛型很好的幫我們解決了上面的問(wèn)題,但是隨之而來(lái)出現(xiàn)了新的問(wèn)題,我們都知道 Int 、 Float 、 Double 是 Number 子類(lèi)型, 因此下面的代碼是可以正常運(yùn)行的。
// Kotlin val number: Number = 1 // Java Number number = 1;
我們花三秒鐘思考一下,下面的代碼是否可以正常編譯。
List<Number> numbers = new ArrayList<Integer>();
答案是不可以,正如下圖所示,編譯會(huì)出錯(cuò)。

這也就說(shuō)明了泛型是不可變的,IDE 認(rèn)為 ArrayList<Integer> 不是 List<Number> 子類(lèi)型,不允許這么賦值,那么如何解決這個(gè)問(wèn)題呢,這就需要用到協(xié)變了,協(xié)變?cè)试S上面的賦值是合法的。
Kotlin 和 Java 的協(xié)變
- 在 Java 中用通配符
? extends T表示協(xié)變,extends限制了父類(lèi)型T,其中?表示未知類(lèi)型,比如? extends Number,只要聲明時(shí)傳入的類(lèi)型是Number或者Number的子類(lèi)型都可以 - 在 Kotlin 中關(guān)鍵字
out T表示協(xié)變,含義和 Java 一樣
現(xiàn)在我們將上面的代碼修改一下,在花三秒鐘思考一下,下面的代碼是否可以正常編譯。
// kotlin val numbers: MutableList<out Number> = ArrayList<Int>() // Java List<? extends Number> numbers = new ArrayList<Integer>();
答案是可以正常編譯,協(xié)變通配符 ? extends Number 或者 out Number 表示接受 Number 或者 Number 子類(lèi)型為對(duì)象的集合,協(xié)變放寬了對(duì)數(shù)據(jù)類(lèi)型的約束,但是放寬是有代價(jià)的,我們?cè)诨ㄈ腌娝伎家幌?,下面的代碼是否可以正常編譯。
// Koltin val numbers: MutableList<out Number> = ArrayList<Int>() numbers.add(1) // Java List<? extends Number> numbers = new ArrayList<Integer>(); numbers.add(1)
調(diào)用 add() 方法會(huì)編譯失敗,雖然協(xié)變放寬了對(duì)數(shù)據(jù)類(lèi)型的約束,可以接受 Number 或者 Number 子類(lèi)型為對(duì)象的集合,但是代價(jià)是 無(wú)法添加元素,只能獲取元素,因此協(xié)變只能作為生產(chǎn)者,向外提供數(shù)據(jù)。
為什么無(wú)法添加元素
因?yàn)?? 表示未知類(lèi)型,所以編譯器也不知道會(huì)往集合中添加什么類(lèi)型的數(shù)據(jù),因此索性不允許往集合中添加元素。
但是如果想讓上面的代碼編譯通過(guò),想往集合中添加元素,這就需要用到逆變了。
Kotlin 和 Java 的逆變
逆變其實(shí)是把繼承關(guān)系顛倒過(guò)來(lái),比如 Integer 是 Number 的子類(lèi)型,但是 Integer 加逆變通配符之后,Number 是 ? super Integer 的子類(lèi),如下圖所示。

- 在 Java 中用通配符
? super T表示逆變,其中?表示未知類(lèi)型,super主要用來(lái)限制未知類(lèi)型的子類(lèi)型T,比如? super Number,只要聲明時(shí)傳入是Number或者Number的父類(lèi)型都可以 - 在 Kotlin 中關(guān)鍵字
in T表示逆變,含義和 Java 一樣
現(xiàn)在我們將上面的代碼簡(jiǎn)單修改一下,在花三秒鐘思考一下是否可以正常編譯。
// Kotlin val numbers: MutableList<in Number> = ArrayList<Number>() numbers.add(100) // Java List<? super Number> numbers = new ArrayList<Number>(); numbers.add(100);
答案可以正常編譯,逆變通配符 ? super Number 或者關(guān)鍵字 in 將繼承關(guān)系顛倒過(guò)來(lái),主要用來(lái)限制未知類(lèi)型的子類(lèi)型,在上面的例子中,編譯器知道子類(lèi)型是 Number,因此只要是 Number 的子類(lèi)都可以添加。
逆變可以往集合中添加元素,那么可以獲取元素嗎?我們花三秒鐘時(shí)間思考一下,下面的代碼是否可以正常編譯。
// Kotlin val numbers: MutableList<in Number> = ArrayList<Number>() numbers.add(100) numbers.get(0) // Java List<? super Number> numbers = new ArrayList<Number>(); numbers.add(100); numbers.get(0);
無(wú)論調(diào)用 add() 方法還是調(diào)用 get() 方法,都可以正常編譯通過(guò),現(xiàn)在將上面的代碼修改一下,思考一下是否可以正常編譯通過(guò)。
// Kotlin val numbers: MutableList<in Number> = ArrayList<Number>() numbers.add(100) val item: Int = numbers.get(0) // Java List<? super Number> numbers = new ArrayList<Number>(); numbers.add(100); int item = numbers.get(0);
調(diào)用 get() 方法會(huì)編譯失敗,因?yàn)?numbers.get(0) 獲取的的值是 Object 的類(lèi)型,因此它不能直接賦值給 int 類(lèi)型,逆變和協(xié)變一樣,放寬了對(duì)數(shù)據(jù)類(lèi)型的約束,但是代價(jià)是 不能按照泛型類(lèi)型讀取元素,也就是說(shuō)往集合中添加 int 類(lèi)型的數(shù)據(jù),調(diào)用 get() 方法獲取到的不是 int 類(lèi)型的數(shù)據(jù)。
對(duì)這一小節(jié)內(nèi)容,我們簡(jiǎn)單的總結(jié)一下。
| 關(guān)鍵字(Java/Kotlin) | 添加 | 讀取 | |
|---|---|---|---|
| 協(xié)變 | ? extends / out | ? | ? |
| 逆變 | ? super / in | ? | ? |
Kotlin 和 Java 數(shù)組協(xié)變的不同之處
無(wú)論是 Kotlin 還是 Java 它們協(xié)變和逆變的含義的都是一樣的,只不過(guò)通配符不一樣,但是他們也有不同之處。
Java 是支持?jǐn)?shù)組協(xié)變,代碼如下所示:
Number[] numbers = new Integer[10];
但是 Java 中的數(shù)組協(xié)變有缺陷,將上面的代碼修改一下,如下所示。
Number[] numbers = new Integer[10]; numbers[0] = 1.0;
可以正常編譯,但是運(yùn)行的時(shí)候會(huì)崩潰。

因?yàn)樽铋_(kāi)始我將 Number[] 協(xié)變成 Integer[],接著往數(shù)組里添加了 Double 類(lèi)型的數(shù)據(jù),所以運(yùn)行會(huì)崩潰。
而 Kotlin 的解決方案非常的干脆,不支持?jǐn)?shù)組協(xié)變,編譯的時(shí)候就會(huì)出錯(cuò),對(duì)于數(shù)組逆變 Koltin 和 Java 都不支持。
協(xié)變和逆變的應(yīng)用場(chǎng)景
協(xié)變和逆變應(yīng)用的時(shí)候需要遵循 PECS(Producer-Extends, Consumer-Super)原則,即 ? extends 或者 out 作為生產(chǎn)者,? super 或者 in 作為消費(fèi)者。遵循這個(gè)原則的好處是,可以在編譯階段保證代碼安全,減少未知錯(cuò)誤的發(fā)生。

協(xié)變應(yīng)用
- 在 Java 中用通配符
? extends表示協(xié)變 - 在 Kotlin 中關(guān)鍵字
out表示協(xié)變
協(xié)變只能讀取數(shù)據(jù),不能添加數(shù)據(jù),所以只能作為生產(chǎn)者,向外提供數(shù)據(jù),因此只能用來(lái)輸出,不用用來(lái)輸入。
在 Koltin 中一個(gè)協(xié)變類(lèi),參數(shù)前面加上 out 修飾后,這個(gè)參數(shù)在當(dāng)前類(lèi)中 只能作為函數(shù)的返回值,或者修飾只讀屬性 ,代碼如下所示。
// 正常編譯
interface ProduceExtends<out T> {
val num: T // 用于只讀屬性
fun getItem(): T // 用于函數(shù)的返回值
}
// 編譯失敗
interface ProduceExtends<out T> {
var num : T // 用于可變屬性
fun addItem(t: T) // 用于函數(shù)的參數(shù)
}
當(dāng)我們確定某個(gè)對(duì)象只作為生產(chǎn)者時(shí),向外提供數(shù)據(jù),或者作為方法的返回值時(shí),我們可以使用 ? extends 或者 out。
- 以 Kotlin 為例,例如
Iterator#next()方法,使用了關(guān)鍵字out,返回集合中每一個(gè)元素

- 以 Java 為例,例如
ArrayList#addAll()方法,使用了通配符? extends

傳入?yún)?shù) Collection<? extends E> c 作為生產(chǎn)者給 ArrayList 提供數(shù)據(jù)。
逆變應(yīng)用
- 在 Java 中使用通配符
? super表示逆變 - 在 Kotlin 中使用關(guān)鍵字
in表示逆變
逆變只能添加數(shù)據(jù),不能按照泛型讀取數(shù)據(jù),所以只能作為消費(fèi)者,因此只能用來(lái)輸入,不能用來(lái)輸出。
在 Koltin 中一個(gè)逆變類(lèi),參數(shù)前面加上 in 修飾后,這個(gè)參數(shù)在當(dāng)前類(lèi)中 只能作為函數(shù)的參數(shù),或者修飾可變屬性 。
// 正常編譯,用于函數(shù)的參數(shù)
interface ConsumerSupper<in T> {
fun addItem(t: T)
}
// 編譯失敗,用于函數(shù)的返回值
interface ConsumerSupper<in T> {
fun getItem(): T
}
當(dāng)我們確定某個(gè)對(duì)象只作為消費(fèi)者,當(dāng)做參數(shù)傳入時(shí),只用來(lái)添加數(shù)據(jù),我們使用通配符 ? super 或者關(guān)鍵字 in,
- 以 Kotlin 為例,例如擴(kuò)展方法
Iterable#filterTo(),使用了關(guān)鍵字in,在內(nèi)部只用來(lái)添加數(shù)據(jù)

- 以 Java 為例,例如
ArrayList#forEach()方法,使用了通配符? super

不知道小伙伴們有沒(méi)有注意到,在上面的源碼中,分別使用了不同的泛型標(biāo)記符 T 和 E,其實(shí)我們稍微注意一下,在源碼中有幾個(gè)高頻的泛型標(biāo)記符 T 、 E 、 K 、 V 等等,它們分別應(yīng)用在不同的場(chǎng)景。
| 標(biāo)記符 | 應(yīng)用場(chǎng)景 |
|---|---|
| T(Type) | 類(lèi) |
| E(Element) | 集合 |
| K(Key) | 鍵 |
| V(Value) | 值 |
以上就是Kotlin與Java 泛型缺陷和應(yīng)用場(chǎng)景詳解的詳細(xì)內(nèi)容,更多關(guān)于Kotlin Java 泛型缺陷的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Java中的SynchronousQueue阻塞隊(duì)列使用代碼實(shí)例
這篇文章主要介紹了Java中的SynchronousQueue阻塞隊(duì)列使用代碼實(shí)例,SynchronousQueue是無(wú)緩沖區(qū)的阻塞隊(duì)列,即不能直接向隊(duì)列中添加數(shù)據(jù),會(huì)報(bào)隊(duì)列滿異常,需要的朋友可以參考下2023-12-12
java實(shí)現(xiàn)微信App支付服務(wù)端
這篇文章主要為大家詳細(xì)介紹了java實(shí)現(xiàn)微信App支付服務(wù)端,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-10-10
深入了解Java中循環(huán)結(jié)構(gòu)的使用
Java中有三種主要的循環(huán)結(jié)構(gòu):while 循環(huán)、do…while 循環(huán)和for 循環(huán)。本文將來(lái)和大家一起講講Java中這三個(gè)循環(huán)的使用,需要的可以參考一下2022-08-08
mybatis通過(guò)if語(yǔ)句實(shí)現(xiàn)增刪改查操作
這篇文章主要介紹了mybatis通過(guò)if語(yǔ)句實(shí)現(xiàn)增刪改查操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-11-11
使用Java代碼實(shí)現(xiàn)RocketMQ的生產(chǎn)與消費(fèi)消息
這篇文章介紹一下其他的小組件以及使用Java代碼實(shí)現(xiàn)生產(chǎn)者對(duì)消息的生成,消費(fèi)者消費(fèi)消息等知識(shí)點(diǎn),并通過(guò)代碼示例介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作有一定的幫助,需要的朋友可以參考下2024-07-07
親手教你IDEA2020.3創(chuàng)建Javaweb項(xiàng)目的步驟詳解
這篇文章主要介紹了IDEA2020.3創(chuàng)建Javaweb項(xiàng)目的步驟詳解,本文是小編手把手教你,通過(guò)圖文并茂的形式給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2021-03-03
功能強(qiáng)大的TraceId?搭配?ELK使用詳解
這篇文章主要為大家介紹了功能強(qiáng)大的TraceId?搭配?ELK使用詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-09-09
高并發(fā)下如何避免重復(fù)數(shù)據(jù)產(chǎn)生技巧
這篇文章主要為大家介紹了高并發(fā)下如何避免重復(fù)數(shù)據(jù)的產(chǎn)生技巧詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-07-07
java前后端使用ajax數(shù)據(jù)交互問(wèn)題(簡(jiǎn)單demo)
這篇文章主要介紹了java前后端使用ajax數(shù)據(jù)交互問(wèn)題(簡(jiǎn)單demo),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。2023-06-06

