泛型¶
Kotlin 泛型与 Java 泛型有相同的目标:在编译期表达类型约束,减少运行期类型错误。但 Kotlin 用 out、in 和类型投影替代了 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。
使用点类型投影¶
有些类型不能在声明处固定为 out 或 in,例如数组既能读又能写:
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