Kotlin系列——泛型型变

本文章已授权微信公众号郭霖(guolin_blog)转载。

本文章讲解的内容是泛型型变,我写一个扩展Boolean示例代码来应用我要讲的内容,示例代码如下:

BooleanExtensionDemo

先看下以下例子,代码如下:

List<String> strings = new ArrayList<String>();
// Java中禁止这样的操作
List<Object> objects = strings;

Java中是禁止这样的操作的,我们看下Kotlin的写法,代码如下:

val strings: List<String> = arrayListOf()
val anys: List<Any> = strings

Kotlin中是允许这样的操作的,这是为什么呢?下面会详细解释。

List<String>中,List基础类型String类型实参,现有两个List集合,分别是List<String>List<Any>,它们都具有相同基础类型,但是类型实参不相同,并且StringAny存在父子关系型变就是指List<String>List<Any>这两者存在什么关系。

形式参数和实际参数

函数中的形参和实参

代码如下:

fun add(firstNumber: Int, secondNumber: Int): Int =
    firstNumber + secondNumber

firstNumbersecondNumber就是形式参数,然后去调用这个函数,代码如下:

val first = 1
val second = 2
add(first, second)

firstsecond就是add函数实际参数

泛型中的形参和实参

代码如下:

class Fruit<T>(var item: T)

T就是类型形参,然后使用这个,代码如下:

val fruit = Fruit<Int>(100)

Int就是Fruit类型实参,因为Kotlin具有类型推导特性,不必明确指明类型,所以其实可以写成如下代码:

val fruit = Fruit(100)

在这种情况下,Int依然是Fruit类型实参

还有以下情况,请看代码:

// Collections.kt
public interface MutableList<E> : List<E>, MutableCollection<E> {
    // 省略部分代码
}

这里的EListMutableCollection类型实参,同时是MutableList类型形参

结论

定义在里面就是形式参数,定义在外面就是实际参数。

子类、超类、子类型、超类型

子类继承超类,例如class Apple: Fruit()Apple就是Fruit子类Fruit就是Apple超类,那什么是子类型超类型呢?它们的规则比子类超类更加宽松,如果需要A类型的地方,都可以用B类型来代替,那么B类型就是A类型的子类型,A类型就是B类型的超类型,例如StringString?,如果一个函数接收的是String?,我们传入的是String的话,编译器是不会报错的,但是如果一个函数接受的是String,我们传入的是String?的话,编译器就会提示我们可能会存在空指针的问题,所以String就是String?的子类型,String?就是String的超类型。

子类型化关系

如果需要A类型的地方,都可以用B类型来代替,那么B类型就是A类型的子类型,B类型到A类型之间的映射关系就是子类型化关系,举个例子:List<String>List<Any>子类型,所以List<String>List<Any>之间存在子类型化关系List<String>List<String?>子类型,所以List<String>List<String?>之间存在子类型化关系MutableList<String>MutableList<Any>之间就没有关系,这个会在下面解释。

协变

协变(convariant)就是保留子类型化关系保证泛型内部操作该类型时是只读的,在Java中,带extends限定(上界)通配符类型使得类型是协变的。

因为List<out E>协变StringAny子类型StringString?子类型,所以List<String>List<Any>子类型List<String>List<String?>子类型

out协变点

以下代码是标准out协变点

// T被声明为out
interface Producer<out T> {

    // T作为只读属性的类型
    val value: T

    // T作为函数返回值的类型
    fun produce(): T

    // T作为只读属性的类型List泛型的类型实参
    val list: List<T>

    // T作为函数返回值的类型List泛型的类型实参
    fun produceList(): List<T>

}

out协变点基本特征:出现的位置是只读属性的类型或者函数的返回值类型,它作为生产者的角色,请求向外部输出。

源码分析

源码中,最为代表性就是List<out E>,代码如下:

// Collections.kt
// E被声明为out
public interface List<out E> : Collection<E> {

    override val size: Int
    override fun isEmpty(): Boolean

    // E作为函数形参类型,而且还加上了@UnsafeVariance注解,下面会解释
    override fun contains(element: @UnsafeVariance E): Boolean

    // E作为函数返回值的类型Iterator泛型的类型实参
    override fun iterator(): Iterator<E>

    // E作为函数形参的类型Collection泛型的类型实参,而且还加上了@UnsafeVariance注解,下面会解释
    override fun containsAll(elements: Collection<@UnsafeVariance E>): Boolean

    // E作为函数返回值的类型
    public operator fun get(index: Int): E

    // E作为函数形参类型,而且还加上了@UnsafeVariance注解,下面会解释
    public fun indexOf(element: @UnsafeVariance E): Int

    // E作为函数形参类型,而且还加上了@UnsafeVariance注解,下面会解释
    public fun lastIndexOf(element: @UnsafeVariance E): Int

    // E作为函数返回值的类型ListIterator泛型的类型实参
    public fun listIterator(): ListIterator<E>

    // E作为函数返回值的类型ListIterator泛型的类型实参
    public fun listIterator(index: Int): ListIterator<E>

    // E作为函数返回值的类型List泛型的类型实参
    public fun subList(fromIndex: Int, toIndex: Int): List<E>

}

逆变

逆变(contravariance)就是反转子类型化关系保证泛型内部操作该类型时是只写的,在Java中,带super限定(下界)通配符类型使得类型是逆变的。

因为Comparable<in T>逆变StringAny子类型StringString?子类型,所以Comparable<Any>Comparable<String>子类型Comparable<String?>Comparable<String>子类型

in逆变点

以下代码是标准in逆变点

// T被声明为in
interface Consumer<in T> {

    // T作为函数形参类型
    fun consume(value: T)

    // T作为函数形参的类型List泛型的类型实参
    fun consumeList(list: List<T>)

}

in逆变点基本特征:出现的位置是函数形参类型,它作为消费者,请求外部输入。

源码分析

源码中,最为代表性就是Comparable<in T>,代码如下:

// Comparable.kt
// T被声明为in
public interface Comparable<in T> {

    // T作为函数形参类型
    public operator fun compareTo(other: T): Int

}

不型变

不型变就是既不被声明为out,也不被声明为in泛型

因为MutableList<E>不型变,虽然StringAny子类型StringString?子类型,但是MutableList<String>MutableList<Any>之间没有任何关系MutableList<String>MutableList<String?>之前没有任何关系

不型变的基本特征:可以出现在任何位置。

源码分析

源码中,最为代表性就是MutableList<E>,代码如下:

// Collections.kt
public interface MutableList<E> : List<E>, MutableCollection<E> {

    // E作为函数形参类型
    override fun add(element: E): Boolean

    // E作为函数形参类型
    override fun remove(element: E): Boolean

    // E作为函数形参的类型Collection泛型的类型实参
    override fun addAll(elements: Collection<E>): Boolean

    // E作为函数形参的类型Collection泛型的类型实参
    public fun addAll(index: Int, elements: Collection<E>): Boolean

    // E作为函数形参的类型Collection泛型的类型实参
    override fun removeAll(elements: Collection<E>): Boolean

    // E作为函数形参的类型Collection泛型的类型实参
    override fun retainAll(elements: Collection<E>): Boolean
    override fun clear(): Unit

    // E作为函数形参类型
    public operator fun set(index: Int, element: E): E

    // E作为函数形参类型
    public fun add(index: Int, element: E): Unit

    // E作为函数返回值的类型
    public fun removeAt(index: Int): E

    // E作为函数返回值的类型MutableListIterator泛型的类型实参
    override fun listIterator(): MutableListIterator<E>

    // E作为函数返回值的类型MutableListIterator泛型的类型实参
    override fun listIterator(index: Int): MutableListIterator<E>

    // E作为函数返回值的类型MutableList泛型的类型实参
    override fun subList(fromIndex: Int, toIndex: Int): MutableList<E>

}

@UnsafeVariance

在上面说的List<out E>源码中,我们发现虽然List<out E>协变的,但是有时出现的位置是逆变的位置,这是为什么呢?其实是可以出现在任何位置上,但是要保证以下两点定义:协变保证泛型内部操作类型时是只读的,逆变保证泛型内部操作类型时是只写的,大体上要遵循上面说的那几个out协变点和in逆变点

我们可以通过加上@UnsafeVariance注解告诉编译器这个地方是合法安全,让其通过编译,如果不加的话,编译器会认为你这里是不合法,编译不通过。

例如上面说的List<out E>源码中,有一个contains函数,这个函数的作用是检查此元素是否包含在此集合中,它的实现方法没有出现写操作,所以这里就可以加上@UnsafeVariance注解,让其通过编译器。

使用处型变和声明处型变

Java是使用使用处型变,有如下接口

public interface IGeneric<T> {
    // 省略部分代码
}

Java禁止这样的操作的:

private void setData(IGeneric<String> item) {
    // Java禁止这样的操作
    IGeneric<Object> newItem = item;
}

我们应该写成如下这样:

private void setData(IGeneric<String> item) {
    IGeneric<? extends Object> newItem = item;
}

我们必须把newItem声明为IGeneric<? extends Object>,类型变得更复杂了,复杂的类型并没有给我们带来任何价值,这种就叫做使用处型变,我们看下Kotlin的写法吧,有如下接口

// T被声明为out
interface IGeneric<out T> {
    // 省略部分代码
}

有如下方法

private fun setData(item: IGeneric<String>) {

    // 泛型IGeneric的类型实参是Any
    val newItem: IGeneric<Any> = item

}

这种就做声明处型变,我们只需要在用out修饰符修饰T即可,语义简单了很多,当然Kotlin也可以使用使用处型变的,我们不再用out修饰符修饰T,代码如下:

interface IGeneric<T> {
    // 省略部分代码
}

然后我们在声明类型的时候加上out修饰符,代码如下:

private fun setData(item: IGeneric<String>) {

    // 泛型IGeneric的类型实参Any被声明为out
    val newItem: IGeneric<out Any> = item

}

星投影

定义

有时候,我们对类型参数一无所知,但是仍然希望以安全的方式使用它,我们可以使用星投影这个泛型类型的每个具体实例化是这个投影的子类型

语法

  • 对于Function<out T : String>T泛型Function的一个具有上界String协变类型参数Function<>等价于Function<out String>,这意味着当T未知时,我们可以安全地从Function<>读取String**的值。
  • 对于Function<in T>T泛型Function的一个逆变类型参数Function<>等价于Function<in Nothing>,这意味着当T未知时,我们不能安全地写入Function<>**。
  • 对于Function<T : String>T泛型Function的一个不型变类型参数Function<>读取值时等价于Function<out String>写入值时等价于Function<in Nothing>*。

如果一个泛型类型具有多个类型参数,那么它们每个类型参数都可以单独投影,例如:如果类型被声明为Function<in T, out U>,那么它的星投影就如下:

  • Function<, String>表示Function<in Nothing, String>*
  • Function<String, >表示Function<String, out Any?>*
  • Function<, >表示Function<in Nothing, out Any?>

要注意的是星投影非常像Java原始类型,但是是安全的。

这里解释一下NothingNothing是所有类型的子类型,源码如下:

public class Nothing private constructor()

应用

我们可以扩展Boolean,让其更具有函数式编程的味道,让链式调用更加顺滑,代码如下:

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
    }

调用地方,代码如下:

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", "是偶数集合") }
            .otherwise { Log.i("TanJiaJun", "不是偶数集合") }
    }

}

我们可以看到密封类BooleanExt,它是个泛型T是一个协变类型参数,为什么要用到协变呢?我们可以观察到T都出现在out协变点,所以T可以被声明为out

我们还看到对象Otherwise继承密封类BooleanExt,我使用了Nothing,为什么要使用Nothing呢?因为在Boolean扩展函数yes中返回的是BooleanExt<T>,如果要返回Otherwise,我们就只能使用Nothing,因为Nothing是所有类型的子类型,上面也提及过,所以这样就符合协变定义了。

题外话

PECS原则

PECS原则是指Producer-Extends, Consumer-Super,它是Effective Java提出来的,如果泛型类型实参生产者,那么就应该用extends;如果泛型类型实参消费者,那么就应该用super

密封类

在我的示例代码中,我用到了sealed这个修饰符,它可以声明一个密封类,我这里大概说下密封类

  • 密封类用来表示受限的类继承结构,意思就是当一个值为有限几种的类型,而不能有任何其他类型,其实他们在某种意义上,有点像枚举类扩展,不过枚举类型的值集合是受限的,每个枚举常量只存在一个实例,而密封类一个子类可以有包含状态的多个实例

  • 密封类可以有子类,但是所有子类必须在与密封类自身相同文件中声明。

  • 密封类自身是抽象的,它不能直接实例化,但是可以有抽象成员

  • 密封类不允许有非private构造函数,它的构造函数就是private的。

  • 扩展密封类子类的类可以放在任何位置,而不需要放在同一个文件中

  • 使用密封类还有个好处在于使用when表达式的时候,当我们用when作为表达式的时候,也就像上面示例代码中的otherwise,我是使用了它的结果,而不是作为语句,如果能够验证语句覆盖了所有情况的时候,我们就不需要再为语句添加一个else子句了。

我的GitHub:TanJiaJunBeyond

Android通用框架:Android通用框架

我的掘金:谭嘉俊

我的简书:谭嘉俊

我的CSDN:谭嘉俊

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,417评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,921评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,850评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,945评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,069评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,188评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,239评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,994评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,409评论 1 304
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,735评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,898评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,578评论 4 336
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,205评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,916评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,156评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,722评论 2 363
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,781评论 2 351

推荐阅读更多精彩内容