Kotlin学习笔记13——泛型

前言

上一篇,我们学习了Kotlin中的数据类和密封类,今天继续来学习Kotlin中的泛型。

Kotlin 泛型

泛型,即 “参数化类型”,将类型参数化,可以用在类,接口,方法上。
与 Java 一样,Kotlin 也提供泛型,为类型安全提供保证,消除类型强转的烦恼。

泛型类

class Box<T>(t: T) {
    var value = t
}

泛型接口

interface IAnimal<T> {}

泛型方法

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

这里以泛型类为示例:

class Box<T>(t : T) {
    var value = t
}

fun main(args: Array<String>) {
    var boxInt = Box<Int>(10)
    var boxString = Box<String>("Runoob")

    println(boxInt.value)
    println(boxString.value)
}

输出结果为:

10
Runoob

定义泛型类型变量,可以完整地写明类型参数,如果编译器可以自动推定类型参数,也可以省略类型参数。Kotlin 泛型函数的声明与 Java 相同,类型参数要放在函数名的前面:

fun <T> boxIn(value: T) = Box(value)

// 以下都是合法语句
val box4 = boxIn<Int>(1)
val box5 = boxIn(1)     // 编译器会进行类型推断

在调用泛型函数时,如果可以推断出类型参数,可以省略泛型参数。

以下实例创建了泛型函数 doPrintln,函数根据传入的不同类型做相应处理:

fun main(args: Array<String>) {
    val age = 23
    val name = "runoob"
    val bool = true

    doPrintln(age)    // 整型
    doPrintln(name)   // 字符串
    doPrintln(bool)   // 布尔型
}

fun <T> doPrintln(content: T) {

    when (content) {
        is Int -> println("整型数字为 $content")
        is String -> println("字符串转换为大写:${content.toUpperCase()}")
        else -> println("T 不是整型,也不是字符串")
    }
}

输出结果为:

整型数字为 23
字符串转换为大写:RUNOOB
T 不是整型,也不是字符串

泛型约束

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

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

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

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

类型擦除

Kotlin 为泛型声明执行的类型安全检测仅在编译期进行, 运行时实例不保留关于泛型类型的任何信息。这一点在 Java 中也是类似的。例如,Array、Array的实例都会被擦除为Array<*>,这样带来的好处是保存在内存中的类型信息也就减少了。

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

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

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

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

型变

声明处型变

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

open class Animal
class Dog : Animal()

val array1: Array<Dog > = arrayOf(Dog (), Dog (), Dog ())
val array2: Array<Animal> = array1

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

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

val list1: List<Dog> = listOf(Dog(), Dog(), Dog())
val list2: List<Animal> = list1

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

public class Array<T> {}
public interface List<out E> : Collection<E> {}

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

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

使用 out 使得一个类型参数协变,协变类型参数只能用作输出,可以作为返回值类型但是无法作为入参的类型:

// 定义一个支持型变的类
class Runoob<out A>(val a: A) {
    fun foo(): A {
        return a
    }
}

fun main(args: Array<String>) {
    var strCo: Runoob<String> = Runoob("a")
    var anyCo: Runoob<Any> = Runoob<Any>("b")
    //由于Runoob中的泛型A使用了out,泛型类型的子类型是被保留的,这里可以赋值
    anyCo = strCo
    println(anyCo.foo())   // 输出 a
}

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

val list1: List<Animal> = listOf(Animal(), Animal(), Animal())
val list2: List<Dog> = list1

即反过来赋值,由于B并不是A的父类,会有Type mismatch. Required:List<Dog> Found:List<Animal>错误提示。为了应对这种情况,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修饰符的作用看起来的相对的,A是B的父类,那么Comparable可以是Comparable的父类,体会下区别。

使用处型变

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

val array1: Array<Dog> = arrayOf(Dog(), Dog(), Dog())
val array2: Array<out Animal> = array1

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

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

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

val array1: Array<Dog> = arrayOf(Dog(), Dog(), Dog())
val array2: Array<Animal> = arrayOf(Animal(), Animal(), Animal())
copy(array1, array2)

同样的问题,由于泛型默认不型变的原因,copy(array1, array2)并不能正常工作。
回想一下,在 Java 中类似的问题可以使用通配符类型参数解决这个问题:

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

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

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

修改上边的 copy函数:

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

这样copy函数就能正常的工作了。使用处型变其实也是一种类型投影,from、to此时都是一个类型受限的投影数组,它们只能返回、接收指定类型的数据。
稍微修改下Dog和Animal类:

open class A {
    open fun println(){
        println("A")
    }
}

class B : A() {
    override fun println() {
        println("B")
    }
}

val array1: Array<B> = arrayOf(B(), B(), B())
val array2: Array<A> = arrayOf(A(), A(), A())
copy(array1, array2)
array2.forEach { it.println() }

打印结果:

B
B
B

可以看到原来array2中的对象已经成功被替换。

星号投射

有些时候, 你可能想表示你并不知道类型参数的任何信息, 但是仍然希望能够安全地使用它. 这里所谓"安全地使用"是指, 对泛型类型定义一个类型投射, 要求这个泛型类型的所有的实体实例, 都是这个投射的子类型。

对于这个问题, Kotlin 提供了一种语法, 称为 星号投射(star-projection):

  • 假如类型定义为 Foo , 其中 T 是一个协变的类型参数, 上界(upper bound)为 TUpper ,Foo<> 等价于 Foo . 它表示, 当 T 未知时, 你可以安全地从 Foo<> 中 读取TUpper 类型的值.
  • 假如类型定义为 Foo , 其中 T 是一个反向协变的类型参数, Foo<> 等价于 Foo . 它表示, 当 T 未知时, 你不能安全地向 Foo<> 写入 任何东西.
  • 假如类型定义为 Foo , 其中 T 是一个协变的类型参数, 上界(upper bound)为 TUpper , 对于读取值的场合, Foo<*> 等价于 Foo , 对于写入值的场合, 等价于 Foo .

如果一个泛型类型中存在多个类型参数, 那么每个类型参数都可以单独的投射. 比如, 如果类型定义为interface Function<in T, out U> , 那么可以出现以下几种星号投射:

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

注意: 星号投射与 Java 的原生类型(raw type)非常类似, 但可以安全使用
关于星号投射,其实就是*代指了所有类型,相当于Any?

class A<T>(val t: T, val t2 : T, val t3 : T)
class Apple(var name : String)
fun main(args: Array<String>) {
    //使用类    
    val a1: A<*> = A(12, "String", Apple("苹果"))
    val a2: A<Any?> = A(12, "String", Apple("苹果"))   //和a1是一样的
    val apple = a1.t3    //参数类型为Any
    println(apple)
    val apple2 = apple as Apple   //强转成Apple类
    println(apple2.name)
    //使用数组
    val l:ArrayList<*> = arrayListOf("String",1,1.2f,Apple("苹果"))
    for (item in l){
        println(item)
    }
}

输出结果:

com.example.kotlindemo.Apple@6e2c634b
苹果
String
1
1.2
com.example.kotlindemo.Apple@7c3df479

尾巴

今天的学习笔记就先到这里了,下一篇我们将继续学习Kotlin中的object关键字
老规矩,喜欢我的文章,欢迎素质三连:点赞,评论,关注,谢谢大家!

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值