網站首頁 編程語言 正文
引言
之前就寫過一篇泛型的文章,但是總覺得寫得不夠系統,所以最近對泛型又作了些研究,算是對這篇文章的補充了。
kotlin之泛型
泛型,是為了讓「類」、「接口」、「方法」具有更加通用的使用范圍而誕生的,舉個例子,假如我們不使用泛型,那么一個List中可以裝得下任何對象,這么做的問題就在于,在使用時,需要對類型進行檢查,不然就會轉換異常。 所以,我們需要將這種檢查前置到編譯期,這樣在編寫代碼時,就可以安全的使用不同類型,例如List,我們一看就知道是一個String類型的list,不能放其他類型的元素。 在Java中,由于歷史原因,它并不存在真泛型,Java所有的泛型都是偽泛型,因為Java在編譯期,會執行「泛型擦除」,從而導致在Java字節碼中,不存在類型信息(但是類型會被保存在其它地方,這個后面講)。
正是由于泛型擦除的問題,你甚至可以通過反射繞開泛型的限制,傳遞一個非當前泛型限制的對象。
泛型類型在Java中,通常以一個大寫字母來進行標識,我們并不是一定要寫「T」來表示泛型,但這是一個約定成俗的表示,類似的約束還有下面這些。
- 通用泛型類型:T,S,U,V
- 集合元素泛型類型:E
- 映射鍵-值泛型類型:K,V
- 數值泛型類型:N
要理解Kotlin的泛型,我們最好首先從Java的泛型來學習,畢竟Kotlin的語法糖太多了,Java會更加白話文一點。 首先,Java中的泛型具有「不變性」,也就是說,編譯器會認為List和List是兩個完全不同的類型,當然,不僅僅是List,比如下面這個例子。
open class A
class B : A()
那么Test<A>和Test<B>是不是一個類型呢?必須不是,雖然A和B是父子關系,但Test<A>和Test<B>就不是了,為什么呢?我們站在編譯器的角度來想想,假如它們是同一個類型,那么在Test類中get出來的實例,到底是A還是B呢?所以編譯器為了避免這種不確定性,就否定了Test<A>和Test<B>是一種類型的推斷。 但是這種處理在我們處理泛型業務時,會有很多限制,所以,泛型提供了「型變」來拓展泛型的使用。
協變
協變指的是,當參數具有父子關系時,子類可以作為參數傳遞,而泛型的上界就是其父類。協變通過上界通配符<? extends 父類型>來實現。 實例化時可確定為「父類型的未知類型」,所以它「只能讀不能寫」,因為編譯器不確定到底是哪個子類。 例如下面的代碼。
List<Button> buttons = new ArrayList<Button>();
List<? extends TextView> textViews = buttons;
由于Button是TextView的子類,所以上面的代碼可以正確運行。我們來解釋下上面的代碼。
- 「?」通配符表示這是一個未知類型
- 「extends」上界通配符表示這個類型只能是其子類或者本身
- 這里不僅可以是類,也可以適用于接口
上界通配符還有一個特例,那就是「?」,例如List<?>,實際上就是List<? extends Object>的縮寫。 在Kotlin中,使用的是「*」,即List<*>,實際上就是List<out Any>
簡而言之,協變就是——如果A是B的子類,那么Generic<A>就是Generic<? extends B>的子類型。
協變的限制
我們來看下面的代碼。
List<? extends TextView> textViews = new ArrayList<TextView>();
TextView textView = textViews.get(0);
// Error
textViews.add(textView);
我們來解釋下上面的代碼,首先,我們定義了一個具有泛型上界的list,然后,我們從list中讀取一個元素,這時候,這個元素的返回類型是什么呢編譯器并不知道,但由于泛型上限的存在,所以它一定是TextView及其子類,所以定義為TextView類型,也完全沒有問題。 接下來我們來實現寫入,這時候,就報錯了。
看上去好像沒錯啊,add進去的元素是TextView類型,符合泛型上界的定義啊,但是,這個List的類型定義是<?extends TextView>,編譯器并不知道具體是什么類型,所以它就認為,最好的辦法就是什么都不讓加,多做就是錯,那不如不做。 所以,經過協變之后的泛型,就失去了寫入的能力,它只能用于向外提供數據,也就是「數據生產者Producer」。
逆變
逆變指的是,父類可以作為參數傳遞,但子類必須是其下界。逆變通過下界通配符<? super 子類型>來實現。 實例化時可確定為「子類型的未知類型」,所以「只能寫不能讀」。
不能讀指的是不能讀取為指定的類型,而不是不能調用讀的方法。
例如下面的代碼。
List<? super Button> buttons = new ArrayList<TextView>();
同樣我們來分析下上面的代碼。
- 「?」通配符表示這是一個未知類型
- 「super」下界通配符表示后面的這個類型,只能是它子類或者本身
- 這里不僅可以是類,也可以適用于接口
其實這整個就是協變的反向操作。一個是約束上界,另一個是約束下界,所以對比著,其實很好理解。 簡而言之,逆變就是——如果A是B的子類,那么Generic<B>就是Generic<? super A>的子類型。
逆變的限制
類似的,我們再來看下逆變的限制。
List<? super Button> buttons = new ArrayList<TextView>();
Button button = new Button(context);
buttons.add(button);
Object object = buttons.get(0);
上面的代碼,創建了一個list,它的元素類型的下界是Button,也就是說,這個list里面都是放的Button的父類類型。 所以,當我們創建一個Button,并寫入的時候,是完全可以的,因為它符合我們定義下界的約束。 再來看看讀取呢?當我們從list中讀取一個元素時,由于編譯器只知道它是Button的父類,但是具體是什么類型,它也不知道,所以,編譯器不如將它作為Object這個萬物基類了。 所以說,逆變之后的泛型,失去了讀的能力(因為讀出來都是Object),所以逆變泛型通常都作為「數據消費者Consumer」。
Kotlin型變
泛型讓我們有了可以支持多種類型的能力,型變讓我們有了修改泛型的能力,總結來說:
- 泛型通配符<? extends x>可以使泛型支持協變,但是「只能讀不能寫」,這里的寫,指的是對泛型集合添加元素,如果是remove(int index)或者是clear這種刪除,則不受影響。
- 泛型通配符<? super x>可以使泛型支持逆變,但是「只能寫不能讀」,這里的讀,指的是不能按照泛型類型讀,但如果按照Object讀出來再強轉具體類型,則是可以的。
在學習了Java泛型之后,我們再來看下Kotlin的泛型,這時候你再看,就沒那么復雜了,核心就兩條。
- 使用關鍵字 out 來支持協變,等同于 Java 中的上界通配符 ? extends
- 使用關鍵字 in 來支持逆變,等同于 Java 中的下界通配符 ? super
其實在理解了逆變和協變之后,你會發現out和in這兩個關鍵字真的是「言簡意賅」,out表示輸出,即協變只用于輸出數據,in表示輸入,即逆變只用于寫入數據。Kotlin官網上有個著名的——Consumer in, Producer out,說的就是這個意思。
Kotlin泛型的優化
我們通過這個例子來看下Kotlin對Java泛型的改進。
申明處型變
我們通過下面這個例子來看下Kotlin申明處型變的好處,這是一個生產者與消費者的例子,代碼如下。
// 生產者
class Producer<T> {
fun produce(): T {}
}
val producer: Producer<out TextView> = Producer<Button>()
val textView: TextView = producer.produce()
首先我們來看生產者,對于T類型的Producer,我們要創建它的子類時,就需要使用協變,即Producer,否則它就只能生產Button類型的數據。所以,在Java中,每次獲取數據的時候,都要聲明一次協變,所以Kotlin對其進行了優化,可以在申明處進行協變,代碼如下。
// 生產者
class Producer<out T> {
fun produce(): T {}
}
val producer1: Producer<TextView> = Producer<Button>()
val producer2: Producer<out TextView> = Producer<Button>()
Kotlin約定,當泛型參數T只會用來輸出時,可以在申明類的時候,直接使用協變約束,這樣在調用的時候,就不用額外使用協變了,當然寫了也不會錯。 與此類似的,消費者也是如此。
// 消費者
class Consumer<T> {
fun consume(t: T) {}
}
val consumer: Consumer<in Button> = Consumer<TextView>()
consumer.consume(Button(context))
我們在使用的時候,也是必須使用逆變,借助Kotlin,同樣可以在申明處進行逆變。
// 消費者
class Consumer<in T> {
fun consume(t: T) {}
}
val consumer1: Consumer<Button> = Consumer<TextView>()
val consumer2: Consumer<in Button> = Consumer<TextView>()
這樣在調用的時候,就不用額外使用逆變了,當然寫了也不會錯。
reified
由于在Java會進行泛型擦除,所以編譯器無法在運行時知道一個確切的泛型類型,也就是說,我們無法在運行時,判斷一個對象是否為一個泛型T的實例,例如下面的代碼。
if (item instanceof T) {
System.out.println(item);
}
同樣的,在Kotlin里面也是不行的,畢竟一母同胞。
if (item is T) {
println(item)
}
為了解決這個問題,在Java或者Kotlin中,我們通常會多傳入一個Class類型的參數,然后通過Class.isInstance來判斷類型是否匹配。 但是由于Kotlin支持了內聯函數,所以它提供了一個更加方便的方式來處理這種場景,那就是「reified」配合「inline」來實現。
inline fun <reified T> checkType(item: Any) {
if (item is T) {
println(item)
}
}
不是說好了不能直接對泛型來做類型判斷嗎,為什么這里卻可以呢?這其實就是內聯的作用,雖然這里是對T做判斷,但實際上在編譯時,這里已經被替換成了具體的類型,而不再是泛型T了,所以當然可以使用is來進行類型判斷了。
支持協變的List
在Kotlin中,有兩種List,一種是可變的,一種是不可變的,即MutableList和List,其中List的申明如下,它已經實現的協變,所以Kotlin中的List只能讀而不能寫。
public interface List<out E> : Collection<E>
獲取泛型的具體類型
reified
通過reified和inline配合,我們可以在運行時獲取泛型的具體類型,這是Kotlin的特性,具體的使用方式,上面的文章已經講了一個例子。下面我們再看看幾個比較典型的例子。
fun reifiedClass() {
// normal
val serviceImpl1 = ServiceLoader.load(Service::class.java)
// reified
val serviceImpl2 = loadService<Service>()
}
inline fun <reified T> loadService() {
ServiceLoader.load(T::class.java)
}
interface Service {
fun work()
}
再看一個簡化startActivity的方式。
inline fun <reified T : Activity> Activity.startActivity(bundle: Bundle? = null) {
val intent = Intent(this, T::class.java)
bundle?.let {
intent.putExtras(it)
}
startActivity(intent)
}
startActivity<SampleActivity>()
傳入指定Class
通過傳入具體的Class類型,我們也可以在運行時獲取泛型類型,這個方法是Java和Kotlin都支持的,這個在前面的文章中也提到了。
匿名內部類
匿名內部類會在運行時實例化,這個時候,就可以拿到泛型的具體類型了,示例代碼如下。
open class Test<T>
fun main() {
val innerClass = object : Test<String>() {}
val genericType: Type? = innerClass.javaClass.genericSuperclass
if (genericType is ParameterizedType) {
val type = genericType.actualTypeArguments[0]
// class java.lang.String
}
}
Class類提供了一個方法getGenericSuperclass ,通過它可以獲取到帶泛型信息的父類Type(Java的Class文件會保留繼承的父類或者接口的泛型信息)。 通過對獲取的genericType來判斷是否實現ParameterizedType接口,是說明支持泛型,從而獲取出對應的泛型列表(因為泛型可能有多個)。 這個方式是一個很巧妙的獲取泛型類型的方法,在Gson中,就是通過它來獲取類型的。
val content = Gson().toJson("xxx", object : TypeToken<String>() {}.type)
在使用Gson時,我們需要創建一個繼承自TypeToken的匿名內部類, 并實例化泛型參數TypeToken,這樣我們就可以通過getGenericSuperclass來獲取父類的Type,也就是上面例子中的TypeToken了。
反射
反射自然是可以拿到運行時的具體類型了,代碼如下。
open class Test<T>
class NewTest : Test<String>() {
private val genericType: Type? = javaClass.genericSuperclass
fun test() {
if (genericType is ParameterizedType) {
val type = genericType.actualTypeArguments[0]
// class java.lang.String
}
}
}
通過反射來獲取實際類型,是很大開源庫中都在使用的方式,例如Retrofit,它在內部就是通過method.genericReturnType來獲取泛型的返回類型,通過method.genericParameterTypes來獲取泛型的參數類型。
不過這里大家要好奇了,在文章的一開始,我們就說了,Java的偽泛型,會在編譯時進行泛型擦除,那么反射又是怎么拿到這些泛型信息的呢? 其實,編譯器還是留了一手,申明處的泛型信息,實際上會以Signature的形式,保存到Class文件的Constant pool中,這樣通過反射,就可以拿到具體的泛型類型了。
要注意的是,這里能保留的是申明處的泛型,如果是調用處的泛型,例如方法的傳參,這種就不會被保存了。
PESC
PESC是泛型型變中的一個指導性原則,意為「Producer Extend Consumer Super」,當然在Kotlin中,這句話要改為「Consumer in, Producer out」。 這個原則是從集合的角度出發的,其目的是為了實現集合的多態。
- 如果只是從集合中讀取數據,那么它就是個生產者,可以使用extend
- 如果只是往集合中增加數據,那么它就是個消費者,可以使用super
- 如果往集合中既存又取,那么你不應該用extend或者super
還是舉一個例子來說明,我們可以認為Kotlin是Java的子類,但是List和List卻是兩個無關的類,它們之間沒有繼承關系,而使用List<? extends Java>后,相當于List和List之間也有了繼承關系,從而可以讀取List中不同類型的數據,List就是通過這種方式來實現了集合的多態。
協變和逆變的使用場景
我們來看這樣一段代碼,我們創建了一個copyAll的方法,傳入to和from兩個列表,代碼如下。
fun <T> copyAll(to: MutableList<T>, from: MutableList<T>) {
to.addAll(from)
}
fun main() {
val numberList = mutableListOf<Number>() // to
val intList = mutableListOf(1, 2, 3, 4) // from
copyAll(numberList, intList)// Error
}
但是這段代碼是不能編譯通過的,原因在于to是一個List,而from是一個List,所以類型轉換異常,不能編譯。 但實際上,我們知道Int是可以轉換為Number的,但是編譯器不知道,所以它只能報錯,編譯器需要的,就是我們告訴它,這樣做是安全的,得到了我們的保證,編譯器才能執行編譯。
這個保證是從兩方面來說的,首先我們來看from。 from是一個List,完全可以當做List,所以,要保證「from取出來的元素可以轉為Number類型,而且from不能再有其它寫入」,否則你向一個List中插入了一條Number類型的元素,那就不亂套了。 所以,我們可以對from做協變,讓它只讀不寫,代碼如下。
fun <T> copyAll(to: MutableList<T>, from: MutableList<out T>) {
to.addAll(from)
}
這樣就表示from,只接受T或者T的子類型,也就是說,from只能是Number或者Number的子類型,而此時from是Int類型,所以編譯通過了。 上面是從from的角度做的保證,那么從to方面呢? 對于to來說,我們需要保證「to只能寫入,而不能讀取」。
fun <T> copyAll(to: MutableList<in T>, from: MutableList<T>) {
to.addAll(from)
}
這樣就表示to,只接受T或者T的父類型,也就是說,to只能是Int或者Int的父類型,而此時to是Number類型,所以編譯通過了。
另外,我們將from的簽名改為List,也是可以編譯的,其原因就是Kotlin中的List已經支持協變了。
相信大家通過這個例子,大概能理解協變和逆變的使用方式了。 那么我們在實際的代碼中,要在哪些場景使用協變和逆變呢? 通常來說,泛型參數協變后則表示——「這個參數在當前類中,只能作為函數的返回值,或者是只讀屬性」。
abstract class TestOut<out T> {
abstract val num: T// 只讀屬性
abstract fun getItem(): T// 函數的返回值
abstract var num1 : T// Error 用于可變屬性
abstract fun addItem(t: T)// Error 用于函數的參數
}
而逆變,表示這個參數「只能作為函數的參數,或者修飾可變屬性」。
abstract class TestIn<in T> {
abstract val num: T//Error 只讀屬性
abstract fun getItem(): T//Error 函數的返回值
abstract fun addItem(t: T)// 用于函數的參數
}
原文鏈接:https://juejin.cn/post/7179047508763476029
相關推薦
- 2022-08-21 深入了解C語言中常見的文件操作方法_C 語言
- 2022-05-12 Kotlin 初始化陷阱。初始化注意事項
- 2022-08-20 Python超詳細講解元類的使用_python
- 2022-08-04 Python練習之讀取XML節點和屬性值的方法_python
- 2021-12-01 C語言多維數組數據結構的實現詳解_C 語言
- 2022-04-04 快應用開發自定義事件 快應用層級 圖片對象Image 獲取元素的寬高
- 2022-11-12 C語言字符串與字符數組面試題中最易錯考點詳解_C 語言
- 2022-06-17 一文輕松了解ASP.NET與ASP.NET?Core多環境配置對比_實用技巧
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支