Kotlin 的泛型

泛型类型参数

泛型允许你定义带类型形参的类型,当这种类型的实例被创建出来的时候,类型形参被替换成称为类型实参的具体类型,例如:

List<String>
Map<String,String>

和一般类型一样, kotlin 编译器也常常能推导出类型实参:

// 通过类型推导
val authors = listOf("fanda", "liuhang")
// <String> 是可以省略的
val authors = listOf<String>("fanda", "liuhang")

注意: 如果你创建一个空的列表,这样就没有任何可以推导出类型实参的信息,必须显式指定。

// 在创建列表的函数中说明类型实参
val authors = listOf<String>()

// 在变量声明中说明类型实参
val authors2: List<String> = listOf()

上述是创建空列表的示例,有两种说明实参的方式。

Java 不同,Kotlin 始终要求类型实参要么被显式地说明,要么能被编译器推导出来。因为泛型是 1.5 版本才引入到 Java 的,它必须兼容老版本的,所以它允许使用没有类型参数的泛型类型——所谓的原生态类型。而 Kotlin 从一开始就有泛型,所以它不支持原生态类型,类型实参必须定义。

泛型函数和属性

如果要编写一个使用列表的函数,希望它可以在任何列表上使用,而不是某个具体类型的元素的列表,需要编写一个泛型函数。大部分使用集合的库函数都是泛型的,比如:

fun <T> List<T>.slice(indices: IntRange): List<T>

基本上是和 Java 的声明类似的,在函数名前声明,即可在函数中使用。

还可以给类或接口的方法,顶层函数、扩展属性以及扩展函数声明类型参数。例如下面这个返回列表倒数第二个元素的扩展属性:

// 泛型扩展属性
val <T> List<T>.penultimate: T
    get() = this[size - 2]

println(listOf(1,2,3).penultimate)      //2

注意: 普通(非扩展)属性不能拥有类型参数,不能在一个类的属性中存储多个不同类型值,因为声明泛型普通属性没有任何意义。

声明泛型类

Java 一样,kotlin 通过在类名称后面加上一对尖括号,并把类型参数放在尖括号内来声明泛型类及泛型接口。一旦声明之后,就可以在类的主体内像其他类型一样使用类型参数。

interface List<T> {  // 定义类型参数 T
    operator fun get(index: Int): T     // T当作普通类型返回
}

如果你的类继承了泛型类(或者实现了泛型接口),你就得为基础类型的泛型形参提供一个类型实参,它可以是具体类型或者另一个类型形参:

class StringList : List<String> {   // 只有基类传入了具体类型
    override fun get(index: Int): String {    // 返回的参数变成  String 
        //TODO
    }
}

class ArrayList<T> : List<T> {  // 两个类后面都需要 <> 来定义类型形参

    override fun get(index: Int): T {
        //TODO
    }
}

类型参数约束

类型参数约束可以限制作为泛型类和泛型函数的类型实参的类型。如果你把一个类型指定为泛型类型形参的上界约束,在泛型类型具体的初始化中,其对应的类型实参就必须是这个具体类型或其子类型。你是这样定义约束:把冒号放在类型参数名称之后,作为类型形参上界的类型紧随其后

fun <T : Number> List<T>.sum(): T {}

相当于 Java 中的:

<T extends Number> T sum(List<T> list)

一旦指定了类型形参 T 的上界,你就可以把类型 T 的值当作它的上界的值使用:

fun <T : Number> oneHalf(value: T): Double {    // 定义 Number 为上界
    return value.toDouble() //调用 Number 的方法
}

极少数情况下,需要在一个类型参数上指定多个约束,这里你需要使用不同的语法:

//约束 T 同时是 CharSequence 和 Appendable 的实现类
fun <T> multiGenericLimit(seq: T) where T : CharSequence, T : Appendable {
    if (!seq.endsWith(".")) {   //调用 CharSequence 的方法
        seq.append(".") ////调用 Appendable 的方法
    }
}

让类型形参非空

如果你声明的是泛型类或者泛型函数,任何类型实参,包括那些可空的类型实参,都可以替换它的类型形参。事实上没有指定上界的类型形参将会使用 Any? 这个默认上界:

class Processor<T>{
    fun process(value: T) {
        // 需要安全调用
        value?.hashCode()
    }
}

val p = Processor<String?>()    //可以传入可空类型
print(p.process(null))  //可以传入 null

process 函数中,参数 value 是可空的,尽管 T 并没有使用问号标记。如果你想保证替换类型形参的始终是非空类型,可以通过添加约束来实现,如果你除了可空性之外没有任何限制,可以使用 Any 代替默认的 Any? 作为上界,否则指定其他非空类型做为上界即可。

class Processor<T : Any> {
    fun process(value: T) {
        // 需要安全调用
        value.hashCode()
    }
}

现在就不能再传入可空的类型了,因为可空类型不是 Any 的子类型,而是默认的 Any? 的子类型。

运行时的泛型:擦除和实化类型参数

JVM 上的泛型一般是通过类型擦除实现的,就是说泛型类实例的类型实参在运行时是不保留的。

运行时的泛型:类型检查和转换

Java 一样,kotlin 的泛型在运行时也被擦除了,这意味着泛型类实例不会携带用于创建它的类型实参的信息。例如,如果你创建一个 List<String> 并将一堆字符串放到其中,在运行时你只能看到它是一个 List ,不能识别出列表本打算包含的是哪种类型的元素。

因为类型实参没有被存储下来,所以不能检查他们。例如,你不能判断一个列表是一个包含字符串的列表还是包含其他对象的列表:

// 不能这样判断,类型实参会被擦除
if (value is List<Int>) { }

你只能判断一个值是否是一个 list 或是 set 或是非他对象,而不能判断包含指定类型实参的对象。那么怎么判断呢?可以使用特殊的 * 投影语法来做这种检查:

// 是否是一个列表
if (value is List<*>){}

这种表示拥有未知类型实参的泛型类型,类似于 Java 中的 List<?> 。上面的例子检查了 value 是否是 List ,而并没有得到关于它的元素类型的任何信息。

注意,在 asas? 转换中仍然可以使用一般的泛型类型。但是如果该类有正确的基础类型但类型实参是错误的,转换也不会失败,因为在运行时转换发生的时候类型实参是未知的。因此,这样的转换会导致编译器发出“unchecked cast” 的警告。这仅仅是一个警告,你仍然可以继续使用这个值。

fun printSum(c: Collection<*>) {
    val intList = c as? List<Int> ?: throw IllegalArgumentException("List is expected")
    println(intList.sum())
}

fun testPrintSum() {
    // 6
    printSum(listOf(1,2,3))
    // java.lang.IllegalArgumentException: List is expected
    printSum(setOf(1,2,3))
    // java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Number
    printSum(listOf("a","b","c"))  
}

编译一切正常:编译器只是发出了一个警告,这意味着代码是合法的。如果在一个整型的列表或者 set 上调用该函数,一切都会如预期发生:第一种情况会打印元素之和,第二种情况会抛出 IllegalArgumentException 异常。但如果你传递了一个错误类型的值,如 List<String> ,运行时会得到一个 ClassCastException 。其实类型转换是成功的,因为类型实参会被擦除,但是 sum 函数会尝试从列表中读取 Number 值然后把它们加到一起,把 StringNumber 用的尝试导致了 ClassCastException 异常。

声明带实化类型参数的函数

前面说过,Kotlin 泛型在运行时会被擦除,泛型函数的类型实参也是这样。在调用泛型函数的时候,在函数体中你不能明确它的类型实参:

// 不能通过编译
fun <T> isA(value: Any) = value is T

因为 T 在运行时会被擦除,没有任何关于 T 的信息,那肯定是不能通过编译的。但是,如果声明成内联函数并且用 reified 标记类型参数,就能够将类型形参实化,意味着你可以在运行时引用实际类型实参。

// 实化类型参数后,可以通过编译
inline fun <reified T> isA(value:Any) = value is T

println(isA<String>("abc")) // true
println(isA<String>(123))//false

标准库函数 filterIsInstance 就是使用了实化类型参数的函数,这个函数接收一个集合,选择其中那些指定类的实例,然后返回这些被选中的实例。

// [abc, fanda]
println(listOf("abc",123,4,2,"fanda",12.4).filterIsInstance<String>())

通过指定 <String> 作为函数的类型实参,你表明感兴趣的只是字符串。因此函数的返回类型是 List<String> 。这种情况下,类型实参在运行时是已知的,函数 filterIsInstance 使用它来检查列表中的值是不是指定为该类型实参的类的实例。

下面我们声明一个该函数的简化版本:

// 注意,这里是 List<*> 的扩展,表示任何未知类型的列表
inline fun <reified T> List<*>.filterType(): List<T> {
    val result = mutableListOf<T>()
    for (e in this) {
        if (e is T) {
            result.add(e)
        }
    }
    return result
}

注意: 一个内联函数可以有多个实化类型参数,也可以同时拥有非实化类型和实化类型参数,即不仅要用 inline 标记函数,还需要用 reified 标记类型形参,才能够被实化,没有被 reified 标记的形参是非实化类型的。

注意:之前的内联函数的应用,是为了避免拥有函数类型的形参导致的额外的性能开销,而这里是为了让参数实化。对需要实化的函数,同样需要关注其函数大小,最好把不依赖实化类型参数的代码抽取到单独的非内联函数中。

为什么实化只对内联函数有效?

编译器把实现内联函数的字节码插入每一次调用发生的地方。每次你调用带实化类型参数的函数时,编译器都知道这次特定调用中用作类型实参的切确类型。因此,编译器可以生成引用作为类型实参的具体类的字节码。实际对 filterIsInstance<String> 调用来说,生成的代码和下面这段代码是等价的:

for (element in this) {
    if (element is String) {
        destination.add(element)
    }
}

因为生成的字节码引用了具体类,而不是类型参数,它不会被运行时发生的类型参数擦除影响。注意,reified 类型参数的内联函数不能在 Java 代码中调用。 普通内联函数可以像常规函数那样在 Java 中调用(可以被调用但不能被内联)。带实化参数类型的函数需要额外的处理,来把类型参数的值替换到字节码中,所以他们必须永远是内联的。这样他们不可能用 Java 那样的普通方式调用。

使用实化类型参数代替类引用

如果你是 Android 开发者,显示 Activity 是一个最常用的方法,也可以使用实化类型参数来代替传递作为 java.lang.ClassActivity 类:

inline fun <reified T : Activity> Context.startNewActivity() {
    startActivity(Intent(this, T::class.java))
}

startNewActivity<MainActivity>()

::class.java 的语法展现了如何获取 kotlin 类对应的 java.lang.Class ,这和 Java 中的 Activity.class 是等价的。

实化类型参数的限制

具体来说,可以按下面的方式使用实化类型参数:

  • 用在类型检查和类型转换中( is 、!is 、 as 、 as? )
  • 使用 Kotlin 反射 API( ::class )
  • 获取相应的 java.lang.Class( ::class.java )
  • 作为调用其他函数的类型实参。

不能做下面的这些事情:

  • 创建指定为类型参数的类的实例。
  • 调用类型参数类的伴生对象的方法。
  • 调用带实化类型参数函数的时候使用非实化类型形参作为类型实参。
  • 把类、属性或者非内联函数的类型参数标记成 reified

变型:泛型和子类型化

变型的概念描述了拥有相同基础类型和不同类型实参的(泛型)类型之间是如何关联的,例如: List<String>List<Any> 之间如何关联。

为什么存在变型:给函数传递实参

假如你有一个接收 List<Any> 作为实参的函数。把 List<String> 类型的变量传给这个函数是否安全?毫无疑问,把一个字符串传给一个期望 Any 的函数是安全的,因为 String 继承了 Any 。但 当 StringAny 变成 List 接口的类型实参之后,情况就没有那么简单了。

fun printContents(list: List<Any>) {
    println(list.joinToString ())
}
// abc, ddd
printContents(listOf("abc","ddd"))

这看上去没有问题,可以正常使用。我们来看另一个例子:

addAnswer(listOf("abc","ddd"))  // 编译不通过

这个例子和上面的例子中,区别仅仅是将 List<Any> 变成了 MutableList<Any> ,就无法将泛型为 Stringlist 传递给函数。

原因如下:

把一个字符串列表传给期望 Any 对象列表的函数是否安全。如果函数添加或者替换了列表中的元素就是不安全的,因为这样会产生类型不一致的可能性。在 Kotlin 中,可以通过根据列表是否可变选择合适的接口来轻松地控制。如果函数接收的是只读列表,可以传递具有更具体的元素类型的列表。如果列表是可变的,就不能这么做。

类、类型和子类型

类和类型的区别: 同样的类名称可以用来声明可空类型和非空类型,这意味着每一个 kotlin 类都可以用于构造至少两种类型。

子类型:任何时候如果需要的是类型 A 的值,你都能够使用类型 B 的值(当做 A 的值),类型 B 就称为类型 A 的子类型(A 就是 B 的超类型)。举例来说,IntNumber 的子类型,但 Int 不是 String 的子类型。这个定义还标明了任何类型都可以被认为是它自己的子类型。

子类型和子类的区别: 如果一个类实现了一个接口,它的类型就是接口类型的子类型,但是不是子类,因为是实现关系而不是继承关系。一个非空类型是它的可空类型版本的子类型,比如一个函数接收 Int? 类型参数,可以传入 Int 类型的实参,但是反过来却不行。

编译器在每一次给变量赋值或者给函数传递实参的时候,都要做这项检查,只有值的类型是变量类型的子类型时,才允许变量存储该值。

一个泛型类(例如 MutableList )如果对于任意两种类型 ABMutableList<A> 既不是 MutableList<B> 的子类型也不是它的超类型,它就被称为在该类型参数上是不变型的。 Java 中所有的类都是不变型的(尽管哪些类具体的使用可以标记成可变型的,稍后你就会看到)。

List 类的类型化规则不一样,Kotlin 中的 List 接口表示的是只读集合,如果 AB 的子类型,那么 List<A> 就是 List<B> 的子类型。这样的类或者接口被称为是协变的。

协变:保留子类型化关系

一个协变类是一个泛型类(我们以Producer<T> 为例),对这种类来说,下面的描述是成立的:如果 AB 的子类型,那么 Producer<A> 就是 Producer<B> 的子类型。在 Kotlin 中,要声明类在某个类型参数上是可以协变的,在该类型参数的名称前面加上 out 关键字即可:

interface Producer<out T> {
    fun produce(): T
}

例如,想象一下有这样一个函数,它负责喂养用类 Herd 代表的一群动物,Herd 类的类型参数确定了畜群中动物的类型。

open class Animal {
    fun feed() {...}
}

class Herd<T : Animal> {
    val size: Int
        get() = ...

    operator fun get(i: Int): T {...}
}

fun feeAll(animals: Herd<Animal>) {
    for (i in 0 until animals.size) {
        animals[i].feed()
    }
}

假设这段代码的用户有一群猫需要照顾:

class Cat : Animal() {
    fun cleanLitter() {...}
}

fun takeCareOfCats(cats: Herd<Cat>) {
    for(i in 0 until cats.size) {
        cats[i].cleanLitter()
        // feedAll(cats)  //错误:类型不匹配
    }
}

如果尝试把猫群传给 feedAll 函数,在编译期你就会得到类型不匹配的错误。因为 Herd 类中的类型参数 T 没有用任何变型修饰符,猫群不是畜群的子类。因为 Herd 类有一个类似 ListAPI ,并且不允许它的调用者添加和改变畜群中的动物,所以不会产生类型不一致的安全性问题,可以把它变成协变并相应地修改调用代码。

class Herd<out T: Animal> {    // T 是协变的了
    ...
}

声明成协变之后,上述的代码就不会报类型不匹配了,因为 List<Cat> 已经被协变成 List<Animal> 的子类型了,可以正常使用。

你不能把任何类都变成协变的:这样不安全。让类在某个类型参数变为协变,限制了该类中对该类型参数使用的可能性。要保证类型安全,它只能用在所谓的 out 位置,意味着这个类只能生产类型 T 的值而不能消费它们。

在类成员的声明中类型参数的使用可以分为 in 位置和 out 位置。考虑这样一个类,它声明了一个类型参数 T 并包含了一个使用 T 的函数。如果函数是把 T 当成返回类型,我们说它在 out 位置。这种情况下,该函数生产类型为 T 的值。如果 T 用作函数参数的类型,它就在 in 位置,这样的函数消费类型为 T 的值。

interface Transformer<T> {
                //in位置 //out位置
    fun transform(t: T): T
}

类的类型参数前的 out 关键字要求所有使用 T 的方法只能把 T 放在 out 位置而不能放在 in 位置。这个关键字约束了使用 T 的可能性,这保证了对应子类型关系的安全性。

重申一下,类型参数 T 上的关键字 out 有两层含义:

  • 子类型化会被保留。
  • T 只能用在 out 位置,即只能生产 T,不能消费 T

我们来看一下 ListMutableList 类的定义:

public interface List<out E> : kotlin.collections.Collection<E> {}

public interface MutableList<E> : kotlin.collections.List<E>, kotlin.collections.MutableCollection<E> {

可以看到 List 因为是只读的,即只生产类型 E 的元素,所以 E 是被声明是协变的。而 MutableList 即消费也生产 E ,所以定义成不变型的,这也就是为什么之前的例子中不能正常编译的原因。

注意: 类型形参不光可以直接当作参数类型或者返回类型使用,还可以当作另一个类型的类型实参。

interface List<out T> : Collection<T> {
    fun subList(fromIndex: Int, toIndex: Int): List<T>
}

注意:构造方法的参数即不在 in 位置,也不在 out 位置,即使类型参数声明成了 out ,仍然可以在构造方法参数的声明中使用它。

class Herd<out T: Animal>(vararg animals: T) {...}

如果把类的实例当成一个更泛化的类型的实例使用,变型会防止该实例被误用:不能调用存在潜在危险的方法。构造方法不是那种在实例创建之后还能调用的方法,因此它不会有潜在危险。

然而,如果你在构造方法的参数上使用了关键字 valvar ,就会同时声明一个 getter 和一个 setter(如果属性是可变的)。因此,对只读属性来说,类型参数用在了 out 位置,而可变属性 在 out 位置和 in 位置都使用了它:

class Herd<T: Animal>(var leadAnimal: T, vararg animals: T) {...}

上面这个例子中,T 不能用 out 标记,因为类包含属性 leadAnimalsetter ,它在 in 位置用到了 T

还需要注意的是,位置规则只覆盖了类外部可见的( publicprotectedinternalAPI 。私有方法的参数即不在 in 位置也不在 out 位置。变型规则只会防止外部使用者对类的误用但不会对类自己的实现起作用:

class Herd<out T: Animal>(private var leadAnimal: T, vararg animals: T) {...}

现在可以安全地让 HerdT 上协变,因为属性 leadAnimal 变成了私有的。

逆变:反转子类型化关系

逆变的概念可以被看成是协变的镜像:对一个逆变来说,它的子类型化关系与用作类型实参的类的子类型化关系是相反的。

interface Comparator<in T> {
    fun compare(e1: T, e2: T): Int {...}
}

in 关键字的意思是,对应类型的值是传递进来给这个类的方法的,并且被这些方法消费。和协变的情况类似,约束类型参数的使用将导致特定的子类型化关系。在类型参数 T 上的 in 关键字意味着子类型化被反转了,而且 T 只能用在 in 位置。

协变的、 逆变的和不变型的类

    协变                                        逆变                                    不变型

Producer<out T>                            Consumer<in T>                        MutableList<T>

类的子类型化保留了: Producer<Cat> 是      子类型化反转了:Consumer<Animal>        没有子类型化 Producer<Animal> 的子类型                是Consumer<Cat>的子类型

T 只能在 Out 位置                            T 只能在 in 位置                        T 可以在任何位置

一个类可以在一个类型参数上协变,同时在另外一个类型参数上逆变。 Function 接口就是一个经典的例子,下面是单个参数的 Fuction 声明:

interface Function<in P, out R> {
    operator fun invoke(p: P): R
}

Kotlin 的表达法 (P) -> R 是表达 Function<P, R> 的另一种更具可读性的形式。可以发现用 in 关键字标记的 P(参数类型)只用在 in 位置,而用 out 关键字标记的 R(返回类型)只用在 out 位置。

fun enumerateCats(f: (Cat) -> Number) {...}
fun Animal.getIndex(): Int = ...
enumerateCats(Animal::getIndex)

Kotlin 中这段代码是合法的。 AnimalCat 的超类型,而 IntNumber 的子类型。

总结: 标记为 Out ,需要传入子类型,只能生产元素。标记为 in ,需要传入超类型 ,只能消费元素。没有标记,传入指定类型。

使用点变型:在类型出现的地方指定变型

在类型参数出现的具体位置指定变型修饰符,即使在类型声明时它不能声明成协变或逆变,这被称作声明点变型。我们知道 MutableList 接口是不变型的,因为它同时生产和消费元素。但是,在某个特定函数中只被当成其中一种角色使用的情况挺常见的:要么是生产者要么是消费者。例如:

fun <T> copyData(source: MutableList<T>, des: MutableList<T>) {
    source.forEach {
        des.add(it)
    }
}

上述函数中,尽管两个集合都拥有不变型的类型,但是来源集合只用于读取(生产),而目标集合只用于写入(消费)。这种情况下,集合元素的类型不需要精确匹配,只需要目标接收类型是来源接收类型的超类即可。例如,把一个来源是字符串(String)的集合拷贝到一个接收(Any)任意对象的集合,这样一点问题都没有。

为了让这个函数支持不同类型的列表,可以引入第二个泛型参数:

fun <T : R, R> copyData2(source: MutableList<T>, des: MutableList<R>) {
    source.forEach {
        des.add(it)
    }
}

Kotlin 中其实不需要引入第二个泛型参数,可以使用点变型的方式来修饰那些类型参数只出现在 out 位置(或 in 位置) 方法:

fun <T> copyData3(source: MutableList<out T>, des: MutableList<T>) {
    source.forEach {
        des.add(it)
    }
}

fun <T> copyData4(source: MutableList< T>, des: MutableList<in T>) {
    source.forEach {
        des.add(it)
    }
}

copyData(mutableListOf(1,2,3), mutableListOf<Any>())    // 不能编译,要相同类型
copyData2(mutableListOf(1,2,3), mutableListOf<Any>())    // 正常使用,但是用了两个形参
copyData3(mutableListOf(1,2,3), mutableListOf<Any>())    // 正常使用,使用点变型 out
copyData4(mutableListOf(1,2,3), mutableListOf<Any>())    // 正常使用,使用点变型 in

这种操作也称作类型投影,我们说 source 不是一个常规的 MutableList ,而是一个投影(受限)的 MutableList 。只能调用返回类型是泛型类型参数的那些方法,或者严格的讲,只在 out 位置使用它的方法。编译器禁止调用使用类型参数做实参的那些方法(在 in 位置使用类型参数),因此上述的 source 是用不了 add 方法的,而正好该方法也不需要用到。

当然,实现 copyData 函数的正确方式应该是使用 List<T> 作为 source 实参的类型,因为我们只用了声明在 List 中的方法,并没有用到 MutableList 中的方法,而且 List 类型参数的变型在声明时就指定了。就像 List<out T> 这样。它和 List<T> 是一个意思,因为 List 已经声明成了 class List<out T> 。编译器会发出警告,标明这样的投影是多余的。

Kotlin 的使用点变型直接对应 Java 的限界通配符。 Kotlin 中的 MutableList<out T>Java 中的MutableList<? extends T>是一个意思。 in 投影的 MutableList<in T> 对应到 JavaMutableList<? super T>

星号投影: 使用 * 代替类型参数

前面提到类型检查和转换的时候,我们提到了一种特殊的星号投影语法,可以用它来标明你不知道关于泛型实参的任何信息。例如,一个包含未知类型的元素的列表用这种语法表示为 List<*>

首先需要注意的是 MutableList<*>MutableList<Any?> 不一样。 MutableList<Any?> 这种列表包含的是任意类型的元素。而 MutableList<*> 是包含某种特定类型元素的列表,只是你不知道是哪个类型。因为不知道是哪种类型,所以你不能向列表中写入任何东西,但是从列表中读取元素是可行的。

val list: MutableList<Any?> = mutableListOf("fanda", 1, "wrdf")
val chars = mutableListOf('a', 'b', 'c')
val unkownList: MutableList<*> = if (Random().nextBoolean()) list else chars
unkownList.add(42)    // 编译器禁止调用这个方法
println(unkownList.first())     // 读取元素是安全的

为什么编译器会把 MutableList<*> 当成 out 投影的类型?在这个例子的上下文中,MutableList<*> 投影成了 MutableList<out Any?> ,当你没有任何元素类型信息的时候,读取 Any? 类型的元素任然是安全的,但是向列表中写入元素是不安全的。 KotlinMyType<*> 相当于 Java 中的 MyType<?>

当类型实参的信息并不重要的时候,可以使用星号投影的语法,不需要使用任何在签名中引用类型参数的方法,或者只是读取数据而不关心它的具体类型。例如,可以实现一个接收 List<*> 做参数的 printFirst 函数:

fun printFirst(list: List<*>) {    //不关心是什么类型的列表
    if (list.isNotEmpty()) {
        println(list.first())    // 返回的是 Any?
    }
}

也可以用类型形参的方式:

fun <T> printFirst(list: List<T>) {    
    if (list.isNotEmpty()) {
        println(list.first())    // 返回的是 T
    }
}

小结

  • kotlin 的泛型和 Java 相当接近:它们使用同样的方式声明泛型函数和泛型类。
  • Java 一样,泛型类型的类型实参只在编译期存在。
  • 不能把带类型实参的类型和 is 运算符一起使用,因为类型实参在运行时将被擦除。
  • 内联函数的类型参数可以标记成实化的,允许你在运行时对它们使用 is 检查,以及获得 java.lang.Class 实例。
  • 可以声明一个类在某个类型参数上是协变的,如果该参数只是用在 out 位置。
  • 可以声明一个类在某个类型参数上是逆变的,如果该参数只是用在 in 位置。
  • kotlin 中的只读接口 List 声明成了协变的,这意味着 List<String>List<Any> 的子类型。
  • 当确切的类型实参未知或者不重要的时候,可以使用星号投影语法。