泛型

Kotlin 泛型与 Java 泛型有相同的目标:在编译期表达类型约束,减少运行期类型错误。但 Kotlin 用 outin 和类型投影替代了 Java 中大量 ? extends? super 通配符写法。

基本泛型类

class Box<T>(var value: T)

val intBox = Box(1)
val stringBox = Box("Kotlin")

构造参数通常能让编译器推断类型,因此不必总写:

val box: Box<Int> = Box<Int>(1)

可以简写:

val box = Box(1)

泛型函数

fun <T> singletonList(value: T): List<T> {
    return listOf(value)
}

调用:

val names = singletonList("Ada")

上界

fun <T : Comparable<T>> maxOf(a: T, b: T): T =
    if (a >= b) a else b

T : Comparable<T> 表示 T 必须是 Comparable<T> 的子类型。Java 近似写法:

<T extends Comparable<T>> T maxOf(T a, T b)

多个约束使用 where

fun <T> copyWhenGreater(
    source: List<T>,
    threshold: T
): List<String>
    where T : CharSequence,
          T : Comparable<T> {
    return source.filter { it > threshold }.map { it.toString() }
}

默认不变

MutableList<String> 不是 MutableList<Any> 的子类型:

val strings: MutableList<String> = mutableListOf("A")
// val anys: MutableList<Any> = strings // 不允许

如果允许,就能这样破坏类型安全:

// anys.add(1)
// val text: String = strings[1] // 实际是 Int

这与 Java 泛型不变性的原因相同。

out:生产者

如果一个类型只“生产” T,可以声明为 out T

interface Source<out T> {
    fun next(): T
}

这样 Source<String> 可以赋值给 Source<Any>

fun read(source: Source<Any>) {
    println(source.next())
}

记忆方式:

  • out T:只从里面拿出 T
  • 类似 Java 的 ? extends T
  • 生产者用 out

in:消费者

如果一个类型只“消费” T,可以声明为 in T

interface Sink<in T> {
    fun accept(value: T)
}

记忆方式:

  • in T:只把 T 放进去。
  • 类似 Java 的 ? super T
  • 消费者用 in

Java 的 PECS 原则是 Producer Extends, Consumer Super。Kotlin 可以记成:Producer out, Consumer in。

使用点类型投影

有些类型不能在声明处固定为 outin,例如数组既能读又能写:

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

Array<out Any> 表示在这个函数里只把 from 当生产者使用,因此不能往 from 写入任意 Any

写入场景:

fun fill(dest: Array<in String>, value: String) {
    for (i in dest.indices) {
        dest[i] = value
    }
}

星投影

当你不知道具体类型,但想安全读取时,可以使用 *

fun printAll(values: List<*>) {
    values.forEach { println(it) }
}

List<*> 不是原始类型。它表示“某种未知元素类型的 List”,读取出来的元素通常只能安全地当作 Any?

Java 对比:

List<?> values

reified 类型参数

普通泛型在 JVM 上会类型擦除:

fun <T> isType(value: Any): Boolean {
    // return value is T // 不允许
    return false
}

内联函数可以使用 reified 保留类型信息:

inline fun <reified T> isType(value: Any): Boolean {
    return value is T
}

调用:

println(isType<String>("Kotlin")) // true

reified 只适用于 inline 函数,因为编译器会把函数体展开到调用处,从而知道实际类型。

实践建议

  • 读多写少的 API,考虑把类型参数设计成 out
  • 只接收值、不返回值的 API,考虑 in
  • 不要把星投影当成逃避类型设计的工具。
  • 公共 API 中泛型约束越明确,调用方越少写强制转换。
  • 从 Java 迁移时,把 ? extends T 优先映射为 out T,把 ? super T 优先映射为 in T

参考

  • 官方泛型文档:https://kotlinlang.org/docs/generics.html