本文章已授權(quán)微信公眾號郭霖(guolin_blog)轉(zhuǎn)載骇钦。
本文章講解的內(nèi)容是泛型的型變宰啦,我寫一個擴(kuò)展Boolean的示例代碼來應(yīng)用我要講的內(nèi)容允懂,示例代碼如下:
先看下以下例子汽馋,代碼如下:
List<String> strings = new ArrayList<String>();
// Java中禁止這樣的操作
List<Object> objects = strings;
在Java中是禁止這樣的操作的偷遗,我們看下Kotlin的寫法醉拓,代碼如下:
val strings: List<String> = arrayListOf()
val anys: List<Any> = strings
在Kotlin中是允許這樣的操作的伟姐,這是為什么呢?下面會詳細(xì)解釋亿卤。
在List<String>中愤兵,List是基礎(chǔ)類型,String是類型實參排吴,現(xiàn)有兩個List集合秆乳,分別是List<String>和List<Any>,它們都具有相同的基礎(chǔ)類型,但是類型實參不相同屹堰,并且String和Any存在父子關(guān)系肛冶,型變就是指List<String>和List<Any>這兩者存在什么關(guān)系。
形式參數(shù)和實際參數(shù)
函數(shù)中的形參和實參
代碼如下:
fun add(firstNumber: Int, secondNumber: Int): Int =
firstNumber + secondNumber
firstNumber和secondNumber就是形式參數(shù)扯键,然后去調(diào)用這個函數(shù)睦袖,代碼如下:
val first = 1
val second = 2
add(first, second)
first和second就是add函數(shù)的實際參數(shù)。
泛型中的形參和實參
代碼如下:
class Fruit<T>(var item: T)
T就是類型形參荣刑,然后使用這個類馅笙,代碼如下:
val fruit = Fruit<Int>(100)
Int就是Fruit的類型實參,因為Kotlin具有類型推導(dǎo)特性厉亏,不必明確指明類型董习,所以其實可以寫成如下代碼:
val fruit = Fruit(100)
在這種情況下,Int依然是Fruit的類型實參爱只。
還有以下情況皿淋,請看代碼:
// Collections.kt
public interface MutableList<E> : List<E>, MutableCollection<E> {
// 省略部分代碼
}
這里的E是List和MutableCollection的類型實參,同時是MutableList的類型形參虱颗。
結(jié)論
定義在里面就是形式參數(shù)沥匈,定義在外面就是實際參數(shù)。
子類忘渔、超類高帖、子類型、超類型
子類會繼承超類畦粮,例如class Apple: Fruit()散址,Apple就是Fruit的子類,Fruit就是Apple的超類宣赔,那什么是子類型和超類型呢预麸?它們的規(guī)則比子類和超類更加寬松,如果需要A類型的地方儒将,都可以用B類型來代替吏祸,那么B類型就是A類型的子類型,A類型就是B類型的超類型钩蚊,例如String和String?贡翘,如果一個函數(shù)接收的是String?,我們傳入的是String的話砰逻,編譯器是不會報錯的鸣驱,但是如果一個函數(shù)接受的是String,我們傳入的是String?的話蝠咆,編譯器就會提示我們可能會存在空指針的問題踊东,所以String就是String?的子類型北滥,String?就是String的超類型。
子類型化關(guān)系
如果需要A類型的地方闸翅,都可以用B類型來代替再芋,那么B類型就是A類型的子類型,B類型到A類型之間的映射關(guān)系就是子類型化關(guān)系缎脾,舉個例子:List<String>是List<Any>的子類型祝闻,所以List<String>到List<Any>之間存在子類型化關(guān)系占卧,List<String>是List<String?>的子類型遗菠,所以List<String>到List<String?>之間存在子類型化關(guān)系,MutableList<String>和MutableList<Any>之間就沒有關(guān)系华蜒,這個會在下面解釋辙纬。
協(xié)變
協(xié)變(convariant)就是保留子類型化關(guān)系,保證泛型內(nèi)部操作該類型時是只讀的叭喜,在Java中贺拣,帶extends限定(上界)的通配符類型使得類型是協(xié)變的。
因為List<out E>是協(xié)變捂蕴,String是Any的子類型譬涡,String是String?的子類型,所以List<String>是List<Any>的子類型啥辨,List<String>是List<String?>的子類型涡匀。
out協(xié)變點
以下代碼是標(biāo)準(zhǔn)的out協(xié)變點:
// T被聲明為out
interface Producer<out T> {
// T作為只讀屬性的類型
val value: T
// T作為函數(shù)返回值的類型
fun produce(): T
// T作為只讀屬性的類型List泛型的類型實參
val list: List<T>
// T作為函數(shù)返回值的類型List泛型的類型實參
fun produceList(): List<T>
}
out協(xié)變點基本特征:出現(xiàn)的位置是只讀屬性的類型或者函數(shù)的返回值類型,它作為生產(chǎn)者的角色溉知,請求向外部輸出陨瘩。
源碼分析
在源碼中,最為代表性就是List<out E>级乍,代碼如下:
// Collections.kt
// E被聲明為out
public interface List<out E> : Collection<E> {
override val size: Int
override fun isEmpty(): Boolean
// E作為函數(shù)形參類型舌劳,而且還加上了@UnsafeVariance注解,下面會解釋
override fun contains(element: @UnsafeVariance E): Boolean
// E作為函數(shù)返回值的類型Iterator泛型的類型實參
override fun iterator(): Iterator<E>
// E作為函數(shù)形參的類型Collection泛型的類型實參玫荣,而且還加上了@UnsafeVariance注解甚淡,下面會解釋
override fun containsAll(elements: Collection<@UnsafeVariance E>): Boolean
// E作為函數(shù)返回值的類型
public operator fun get(index: Int): E
// E作為函數(shù)形參類型,而且還加上了@UnsafeVariance注解捅厂,下面會解釋
public fun indexOf(element: @UnsafeVariance E): Int
// E作為函數(shù)形參類型贯卦,而且還加上了@UnsafeVariance注解,下面會解釋
public fun lastIndexOf(element: @UnsafeVariance E): Int
// E作為函數(shù)返回值的類型ListIterator泛型的類型實參
public fun listIterator(): ListIterator<E>
// E作為函數(shù)返回值的類型ListIterator泛型的類型實參
public fun listIterator(index: Int): ListIterator<E>
// E作為函數(shù)返回值的類型List泛型的類型實參
public fun subList(fromIndex: Int, toIndex: Int): List<E>
}
逆變
逆變(contravariance)就是反轉(zhuǎn)子類型化關(guān)系恒傻,保證泛型內(nèi)部操作該類型時是只寫的脸侥,在Java中,帶super限定(下界)的通配符類型使得類型是逆變的盈厘。
因為Comparable<in T>是逆變睁枕,String是Any的子類型斋荞,String是String?的子類型励两,所以Comparable<Any>是Comparable<String>的子類型,Comparable<String?>是Comparable<String>的子類型。
in逆變點
以下代碼是標(biāo)準(zhǔn)的in逆變點:
// T被聲明為in
interface Consumer<in T> {
// T作為函數(shù)形參類型
fun consume(value: T)
// T作為函數(shù)形參的類型List泛型的類型實參
fun consumeList(list: List<T>)
}
in逆變點基本特征:出現(xiàn)的位置是函數(shù)形參類型赏廓,它作為消費者,請求外部輸入嘴高。
源碼分析
在源碼中浸策,最為代表性就是Comparable<in T>,代碼如下:
// Comparable.kt
// T被聲明為in
public interface Comparable<in T> {
// T作為函數(shù)形參類型
public operator fun compareTo(other: T): Int
}
不型變
不型變就是既不被聲明為out菲语,也不被聲明為in的泛型妄辩。
因為MutableList<E>是不型變,雖然String是Any的子類型山上,String是String?的子類型眼耀,但是MutableList<String>和MutableList<Any>之間沒有任何關(guān)系,MutableList<String>和MutableList<String?>之前沒有任何關(guān)系佩憾。
不型變的基本特征:可以出現(xiàn)在任何位置哮伟。
源碼分析
在源碼中,最為代表性就是MutableList<E>妄帘,代碼如下:
// Collections.kt
public interface MutableList<E> : List<E>, MutableCollection<E> {
// E作為函數(shù)形參類型
override fun add(element: E): Boolean
// E作為函數(shù)形參類型
override fun remove(element: E): Boolean
// E作為函數(shù)形參的類型Collection泛型的類型實參
override fun addAll(elements: Collection<E>): Boolean
// E作為函數(shù)形參的類型Collection泛型的類型實參
public fun addAll(index: Int, elements: Collection<E>): Boolean
// E作為函數(shù)形參的類型Collection泛型的類型實參
override fun removeAll(elements: Collection<E>): Boolean
// E作為函數(shù)形參的類型Collection泛型的類型實參
override fun retainAll(elements: Collection<E>): Boolean
override fun clear(): Unit
// E作為函數(shù)形參類型
public operator fun set(index: Int, element: E): E
// E作為函數(shù)形參類型
public fun add(index: Int, element: E): Unit
// E作為函數(shù)返回值的類型
public fun removeAt(index: Int): E
// E作為函數(shù)返回值的類型MutableListIterator泛型的類型實參
override fun listIterator(): MutableListIterator<E>
// E作為函數(shù)返回值的類型MutableListIterator泛型的類型實參
override fun listIterator(index: Int): MutableListIterator<E>
// E作為函數(shù)返回值的類型MutableList泛型的類型實參
override fun subList(fromIndex: Int, toIndex: Int): MutableList<E>
}
@UnsafeVariance
在上面說的List<out E>源碼中楞黄,我們發(fā)現(xiàn)雖然List<out E>是協(xié)變的,但是有時出現(xiàn)的位置是逆變的位置抡驼,這是為什么呢鬼廓?其實是可以出現(xiàn)在任何位置上,但是要保證以下兩點定義:協(xié)變保證泛型內(nèi)部操作類型時是只讀的婶恼,逆變保證泛型內(nèi)部操作類型時是只寫的桑阶,大體上要遵循上面說的那幾個out協(xié)變點和in逆變點。
我們可以通過加上@UnsafeVariance注解告訴編譯器這個地方是合法勾邦、安全蚣录,讓其通過編譯,如果不加的話眷篇,編譯器會認(rèn)為你這里是不合法萎河,編譯不通過。
例如上面說的List<out E>源碼中蕉饼,有一個contains函數(shù)虐杯,這個函數(shù)的作用是檢查此元素是否包含在此集合中,它的實現(xiàn)方法沒有出現(xiàn)寫操作昧港,所以這里就可以加上@UnsafeVariance注解擎椰,讓其通過編譯器。
使用處型變和聲明處型變
Java是使用使用處型變创肥,有如下接口:
public interface IGeneric<T> {
// 省略部分代碼
}
Java是禁止這樣的操作的:
private void setData(IGeneric<String> item) {
// Java禁止這樣的操作
IGeneric<Object> newItem = item;
}
我們應(yīng)該寫成如下這樣:
private void setData(IGeneric<String> item) {
IGeneric<? extends Object> newItem = item;
}
我們必須把newItem聲明為IGeneric<? extends Object>达舒,類型變得更復(fù)雜了值朋,復(fù)雜的類型并沒有給我們帶來任何價值,這種就叫做使用處型變巩搏,我們看下Kotlin的寫法吧昨登,有如下接口:
// T被聲明為out
interface IGeneric<out T> {
// 省略部分代碼
}
有如下方法:
private fun setData(item: IGeneric<String>) {
// 泛型IGeneric的類型實參是Any
val newItem: IGeneric<Any> = item
}
這種就做聲明處型變,我們只需要在用out修飾符修飾T即可贯底,語義簡單了很多丰辣,當(dāng)然Kotlin也可以使用使用處型變的,我們不再用out修飾符修飾T禽捆,代碼如下:
interface IGeneric<T> {
// 省略部分代碼
}
然后我們在聲明類型的時候加上out修飾符笙什,代碼如下:
private fun setData(item: IGeneric<String>) {
// 泛型IGeneric的類型實參Any被聲明為out
val newItem: IGeneric<out Any> = item
}
星投影
定義
有時候,我們對類型參數(shù)一無所知睦擂,但是仍然希望以安全的方式使用它得湘,我們可以使用星投影杖玲,這個泛型類型的每個具體實例化是這個投影的子類型顿仇。
語法
- 對于Function<out T : String>,T是泛型Function的一個具有上界String的協(xié)變類型參數(shù)摆马,Function<>等價于Function<out String>臼闻,這意味著當(dāng)T為未知時,我們可以安全地從Function<>讀取String**的值囤采。
- 對于Function<in T>述呐,T是泛型Function的一個逆變類型參數(shù),Function<>等價于Function<in Nothing>蕉毯,這意味著當(dāng)T為未知時乓搬,我們不能安全地寫入Function<>**。
- 對于Function<T : String>代虾,T是泛型Function的一個不型變類型參數(shù)进肯,Function<>讀取值時等價于Function<out String>,寫入值時等價于Function<in Nothing>*棉磨。
如果一個泛型類型具有多個類型參數(shù)江掩,那么它們每個類型參數(shù)都可以單獨投影,例如:如果類型被聲明為Function<in T, out U>乘瓤,那么它的星投影就如下:
- Function<, String>表示Function<in Nothing, String>*
- Function<String, >表示Function<String, out Any?>*
- Function<, >表示Function<in Nothing, out Any?>
要注意的是星投影非常像Java的原始類型环形,但是是安全的。
這里解釋一下Nothing衙傀,Nothing是所有類型的子類型抬吟,源碼如下:
public class Nothing private constructor()
應(yīng)用
我們可以擴(kuò)展Boolean,讓其更具有函數(shù)式編程的味道统抬,讓鏈?zhǔn)秸{(diào)用更加順滑火本,代碼如下:
package com.tanjiajun.booleanextensiondemo
/**
* Created by TanJiaJun on 2020-01-28.
*/
sealed class BooleanExt<out T>
class TransferData<T>(val data: T) : BooleanExt<T>()
object Otherwise : BooleanExt<Nothing>()
inline fun <T> Boolean.yes(block: () -> T): BooleanExt<T> =
when {
this -> TransferData(block.invoke())
else -> Otherwise
}
inline fun <T> BooleanExt<T>.otherwise(block: () -> T): T =
when (this) {
is Otherwise -> block()
is TransferData -> data
}
調(diào)用地方任洞,代碼如下:
package com.tanjiajun.booleanextensiondemo
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
/**
* Created by TanJiaJun on 2020-01-28.
*/
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 第一個例子
val name = "譚嘉俊"
(name == "譚嘉俊")
.yes { Log.i("TanJiaJun", name) }
.otherwise { Log.i("TanJiaJun", "蘋果") }
// 第二個例子
val strings = mutableListOf(2, 4, 6, 8, 10)
(strings
.filter { it % 2 == 0 }
.count() == strings.size)
.yes { Log.i("TanJiaJun", "是偶數(shù)集合") }
.otherwise { Log.i("TanJiaJun", "不是偶數(shù)集合") }
}
}
我們可以看到密封類BooleanExt,它是個泛型发侵,T是一個協(xié)變類型參數(shù)交掏,為什么要用到協(xié)變呢?我們可以觀察到T都出現(xiàn)在out協(xié)變點刃鳄,所以T可以被聲明為out盅弛。
我們還看到對象Otherwise繼承密封類BooleanExt,我使用了Nothing叔锐,為什么要使用Nothing呢挪鹏?因為在Boolean的擴(kuò)展函數(shù)yes中返回的是BooleanExt<T>,如果要返回Otherwise愉烙,我們就只能使用Nothing讨盒,因為Nothing是所有類型的子類型,上面也提及過步责,所以這樣就符合協(xié)變的定義了返顺。
題外話
PECS原則
PECS原則是指Producer-Extends, Consumer-Super,它是Effective Java提出來的蔓肯,如果泛型的類型實參是生產(chǎn)者遂鹊,那么就應(yīng)該用extends;如果泛型的類型實參是消費者蔗包,那么就應(yīng)該用super秉扑。
密封類
在我的示例代碼中,我用到了sealed這個修飾符调限,它可以聲明一個密封類舟陆,我這里大概說下密封類:
密封類用來表示受限的類繼承結(jié)構(gòu),意思就是當(dāng)一個值為有限幾種的類型耻矮,而不能有任何其他類型秦躯,其實他們在某種意義上,有點像枚舉類的擴(kuò)展淘钟,不過枚舉類型的值集合是受限的宦赠,每個枚舉常量只存在一個實例,而密封類的一個子類可以有包含狀態(tài)的多個實例米母。
密封類可以有子類勾扭,但是所有子類必須在與密封類自身相同的文件中聲明。
密封類自身是抽象的铁瞒,它不能直接實例化妙色,但是可以有抽象成員。
密封類不允許有非private構(gòu)造函數(shù)慧耍,它的構(gòu)造函數(shù)就是private的身辨。
擴(kuò)展密封類子類的類可以放在任何位置丐谋,而不需要放在同一個文件中。
使用密封類還有個好處在于使用when表達(dá)式的時候煌珊,當(dāng)我們用when作為表達(dá)式的時候号俐,也就像上面示例代碼中的otherwise,我是使用了它的結(jié)果定庵,而不是作為語句吏饿,如果能夠驗證語句覆蓋了所有情況的時候,我們就不需要再為語句添加一個else子句了蔬浙。
我的GitHub:TanJiaJunBeyond
Android通用框架:Android通用框架
我的掘金:譚嘉俊
我的簡書:譚嘉俊
我的CSDN:譚嘉俊