Kotlin 泛型

Kotlin 中也有泛型的概念,和 Java 中的类似,但又不尽相同,一起来认识 Kotlin 中的泛型吧。

一、基本使用

通常我们会在类、接口、方法中声明泛型:

1、泛型类

class Animal<T> {}

2、泛型接口

interface IAnimal<T> {}

3、泛型方法

fun <T> initAnimal(param: T) {}

二、泛型约束

泛型约束表示我们可以指定泛型类型(T)的上界,即父类型,默认的上界为Any?,如果只有一个上界可以这样指定:

fun <T : Animal<T>> initAnimal(param: T) {}

Animal<T>就是上界类型,这里使用了:,在 Java 中对应extends关键字,如果需要指定多个上界类型,就需要使用where语句:

fun <T> initAnimal(param: T) where T : Animal<T>, T : IAnimal<T> {}

三、类型擦除

Kotlin 为泛型声明执行的类型安全检测仅在编译期进行, 运行时实例不保留关于泛型类型的任何信息。这一点在 Java 中也是类似的。

例如,Array<String>Array<Int>的实例都会被擦除为Array<*>,这样带来的好处是保存在内存中的类型信息也就减少了。

由于运行时泛型信息被擦除,所以在运行时无法检测一个实例是否是带有某个类型参数的泛型类型,所以下面的代码是无法通过编译的(Cannot check for instance of erased type: Array<Int>):

fun isArray(a: Any) {
    if (a is Array<Int>) {
        println("is array")
    }
}

但我们可以检测一个实例是否是数组,虽然 Kotlin 不允许使用没有指定类型参数的泛型类型,但可以使用星投影*(这个后边会说到):

fun isArray(a: Any) {
    if (a is Array<*>) {
        println("is array")
    }
}

同样原因,由于类型被擦除,我们也无法安全的将一个实现转换成带有某个类型参数的泛型类型:

fun sumArray(a: Array<*>) {
    val intArray = a as? Array<Int> ?: throw IllegalArgumentException("Array的泛型类型必须是Int类型")
    println(intArray.sum())
}

因为我们无法判断数组a的是不是Array<Int>类型的,所以可能会出现异常的情况。

对于泛型函数,如果在函数内需要使用具体的泛型类型,同样由于运行时泛型信息被擦除的原因,你无法直接使用它(Cannot check for instance of erased type: T):

fun < T> test(param: Any) {
    if (param is T){
        println("param type is match")
    }
}

但还是有办法的,可以用inline关键字修饰函数,即内联函数,这样编译器会把每一次函数调用都换成函数实际代码实现,同时用reified关键字修饰泛型类型,这样就能保留泛型参数的具体类型了:

inline fun <reified T> test(param: Any) {
    if (param is T){
        println("param type is match")
    }
}

四、型变

1、声明处型变

型变是泛型中比较重要的概念,首先我们要知道 Kotlin 中的泛型是不型变的,这点和 Java 类似。那什么是型变呢,看个例子:

open class A
class B : A()

val array1: Array<B> = arrayOf(B(), B(), B())
val array2: Array<A> = array1

你会发现第二个赋值语句会有错误提示,Type mismatch. Required:Array<A> Found:Array<B>类型不匹配,Array<B>并不是Array<A>的子类,就是因为 Kotlin 中的泛型是默认不型变的,无法自动完成类型转换,但BA的子类,这个赋值操作本质上是合理的、安全的,但编译器似乎并不知道,这必然给我们开发过程中带来了麻烦。

为什么Array无法正常的赋值,而ListSetMap可以呢?如下代码,编译器不会有错误提示的:

val list1: List<B> = listOf(B(), B(), B())
val list2: List<A> = list1

我们可以对比一下ArrayList在源码中的定义:

public class Array<T> {}

public interface List<out E> : Collection<E> {}

可以看到List的泛型类型使用了out修饰符,这就是关键所在了。这就是 Kotlin 中的声明处型变,用来向编译器解释这种情况。

  • 关于out修饰符我们可这样理解,当类、接口的泛型类型参数被声明为out时,则该类型参数是协变的,泛型类型的子类型是被保留的,它只能出现在函数的输出位置,只能作为返回类型,即生产者。带来的好处是,AB的父类,那么List<A>可以是List<B>的父类。

我们修改下上边List赋值的代码:

val list1: List<A> = listOf(A(), A(), A())
val list2: List<B> = list1

即反过来赋值,由于B并不是A的父类,会有Type mismatch. Required:List<B> Found:List<A>错误提示。为了应对这种情况,Kotlin 还提供了一个in修饰符。

  • 关于in修饰符我们可这样理解,当类、接口的泛型类型参数被声明为in时,则该类型参数是逆变的,泛型类型的父类型是被保留的,它只能出现在函数的输入位置,作为参数,只能作为消费类型,即消费者

其实 Kotlin 中的Comparable接口使用了in修饰符:

public interface Comparable<in T> {
    public operator fun compareTo(other: T): Int
}

写一个测试函数,编译器并不会报错:

fun test(a: Comparable<A>) {
    val b: Comparable<B> = a
}

所以in修饰符和out修饰符的作用看起来的相对的,AB的父类,那么Comparable<B>可以是Comparable<A>的父类,体会下区别。

2、使用处型变

为了能将Array<B>赋值给Array<A>,我们修改下之前的代码:

val array1: Array<B> = arrayOf(B(), B(), B())
val array2: Array<out A> = array1

这就是使用处型变,相比声明处型变,使用处型变就要复杂些,为了完成对应的需求,需要每次使用对应类时都添加型变修饰符。而声明处型变在类、接口声明时就做好了这些工作,因而代码会更加简洁。

再看一个数组拷贝的函数:

fun copy(from: Array<A>, to: Array<A>) {
    for (i in from.indices) {
        to[i] = from[i]
    }
}

我们试着执行如下的拷贝操作:

val array1: Array<B> = arrayOf(B(), B(), B())
val array2: Array<A> = arrayOf(A(), A(), A())
copy(array1, array2)

同样的问题,由于泛型默认不型变的原因,copy(array1, array2)并不能正常工作。

回想一下,在 Java 中类似的问题可以使用通配符类型参数解决这个问题:

public void copy(ArrayList<? extends A> from, ArrayList<? super A> to) {}

那么在 Kotlin 中我们自然想到的是型变修饰符了:

  • Kotlin 中的out A类似于 Java 中的? extends A,即泛型参数类型必须是A或者A子类,用来确定类型的上限
  • Kotlin 中的in A类似于 Java 中的? super A,即泛型参数类型必须是B或者B父类,用来确定类型的下限

修改上边的 copy函数:

fun copy(from: List<out A>, to: List<in A>) {
    for (i in from.indices) {
        to[i] = from[i]
    }
}

这样copy函数就能正常的工作了。使用处型变其实也是一种类型投影fromin此时都是一个类型受限的投影数组,它们只能返回、接收指定类型的数据。

这些概念很容易把人搞晕,理解其作用才是关键,而不是套概念。

五、类型投影

前边我们已经知道使用处型变也是一种类型投影,除此之外还有一种星投影

当我们不知道泛型参数的类型信息时,但仍需要安全的使用它时,可以使用星投影,用星号*表示,星投影和 Java 中的原始类型很像,但星投影是安全。

官方对星投影语法的解释如下:

  • 对于 Foo <out T : TUpper>,其中 T 是一个具有上界 TUpper 的协变类型参数,Foo <> 等价于 Foo <out TUpper>。 这意味着当 T 未知时,你可以安全地从 Foo <> 读取 TUpper 的值。
  • 对于 Foo <in T>,其中 T 是一个逆变类型参数,Foo <> 等价于 Foo <in Nothing>。 这意味着当 T 未知时,没有什么可以以安全的方式写入 Foo <>。
  • 对于 Foo <T : TUpper>,其中 T 是一个具有上界 TUpper 的不型变类型参数,Foo<*> 对于读取值时等价于 Foo<out TUpper> 而对于写值时等价于 Foo<in Nothing>。

如果泛型类型具有多个类型参数,则每个类型参数都可以单独投影。 例如,如果类型被声明为 interface Function <in T, out U>,我们可以想象以下星投影:

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

我们来看如下的代码:

val array1: Array<B> = arrayOf(B(), B(), B())
val array2: Array<*> = array1

使用星投影,我们可以将array1赋值给array2,但由于此时array2并不知道泛型参数的类型,所以不能对array2进行数据写入的操作,但可以从中读取数据:

array2[0] =A() //编译器会报错
val a = array2[0] // 正常

可以看出,星投影更适合那些泛型参数的类型不重要的场景。

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 建议先阅读我的上一篇文章 -- Java 泛型 和 Java 泛型一样,Kotlin 泛型也是 Kotlin 语言...
    JohnnyShieh阅读 6,531评论 1 26
  • 前言 人生苦多,快来 Kotlin ,快速学习Kotlin! 什么是Kotlin? Kotlin 是种静态类型编程...
    任半生嚣狂阅读 26,267评论 9 118
  • 转载文章,出处: https://blog.kotliner.cn/2017/06/26/kotlin-gener...
    _10_01_阅读 707评论 0 0
  • 泛型 泛型(Generic Type)简介 通常情况的类和函数,我们只需要使用具体的类型即可:要么是基本类型,要么...
    Tenderness4阅读 1,436评论 4 2
  • 与Java一样,Kotlin也支持泛型,为类型安全提供保证,消除类型强转的烦恼 创建类的实例时我们需要指定类型参数...
    郎官人阅读 1,244评论 0 0