我们经常会用到 List,它是一个可以存放数据的列表,它不仅能装 String,也能装自定义的 User 对象,这是怎么做到的?
答案是泛型 。它允许我们在定义类、接口和方法时,将类型参数化:即在创建类对象或者调用方法时,类型才会确定为指定的具体类型。
什么意思?举个例子:在没有泛型之前,集合像一个没有任何标签的"万能魔法盒",我们可以往里面扔袜子、扔电池、扔苹果。当我们闭着眼从里面拿东西,并假设它是苹果咬一口时,很可能会吃到臭袜子。这就叫做 ClassCastException 类型转换异常。
有了泛型就相当于给"魔法盒"贴上标签,即使闭着眼,也能安全地从贴着"苹果"标签的魔法盒中拿到可口的苹果。简单来说,它是为了类型安全而出现的"标签",可以让错误在编译期就被发现。
泛型的基本使用
我们来看一个真实的场景:网络请求返回的数据封装。
通常,服务器返回的 JSON 数据结构最外层是固定的,有状态码 code、提示信息 msg,以及具体的数据 data。
其中 data 几乎是一直在变的,这时,就可以用泛型来适配。首先定义一个泛型类:
kotlin
// 泛型语法:<T>,其中的 T 是大家约定俗成的泛型写法,代表 Type
data class ApiResponse<T>(
val code: Int,
val msg: String,
val data: T // data 的类型是泛型 T
)
泛型类内部可以使用该类定义的泛型,不同于待会要讲到的泛型方法,其定义的泛型只能用于本方法。
在具体实例化这个类时,我们可以将泛型指定为具体类型:
kotlin
data class User(val id: Int, val name: String)
data class Order(val id: String)
fun main() {
// 1. 请求用户信息场景中,指定泛型为 User
val userResponse: ApiResponse<User> = ApiResponse(
200,
"Success",
User(0, "John"),
)
// 2. 请求用户订单场景中,指定泛型为 List<Order>
val orderResponse: ApiResponse<List<Order>> = ApiResponse(
200,
"Success",
listOf(
Order("12345")
)
)
}
泛型方法与类型推导
如果你不想定义一个泛型类,只想定义泛型方法,只需要将定义泛型的语法结构写在方法上就行了。
比如标准库中的 apply 函数,就是泛型方法的体现:
kotlin
public inline fun <T> T.apply(block: T.() -> Unit): T {
block()
return this
}
它将泛型定义在了方法上,同时方法中的方法调用者(T)、函数类型参数的接收者(T.())以及返回值(T)的类型都使用了此泛型。
得益于 Kotlin 强大的类型推导机制,该方法的泛型类型可以根据传入的参数或调用者自动推导出,所以很多时候(包括前面的类泛型)我们可以省略泛型的指定:
kotlin
val str = "Hello"
// 编译器自动推断出 T 是 String 类型
// 不需要显式指定 str.apply<String> { ... }
str.apply {
println(this.length)
}
泛型约束:给魔法盒加点规矩
有时候,我们不希望什么东西都能往盒子里装。Kotlin 允许我们通过指定上界的方式来对泛型的具体类型进行限制。
单一约束
比如一个方法只想处理数字类型,可以将方法的泛型上界设为 Number 类型:
kotlin
fun <T : Number> method(param: T): T {
return param
}
有了这个约束后,方法的泛型只能为数字类型,例如 Int、Float、BigDecimal 等。
如果传入的参数是其他类型,如 String,会报错:
less
Type argument is not within its bounds: type parameter 'T (of fun <T : Number> method)' must be subtype of 'Number', but actual: 'String'.
多重约束
如果泛型有多个要求,比如既要是 Animal 的子类,又要实现 Runnable 接口。这时需要用到 where 关键字:
并不支持
<T : Animal, Runnable>这种写法。注意:如果约束中既有类又有接口,类必须写在最前面。
kotlin
interface Runnable {
fun run()
}
open class Animal
// 实现多重约束时,Animal 类必须写在 Runnable 接口前面
fun <T> startRunning(animal: T) where T : Animal, T : Runnable {
animal.run()
}
泛型的可空性
默认情况下,所有泛型的具体类型都是可为空的,因为在不指定泛型的上界时,默认上界就是 Any?。
kotlin
// 泛型 T 默认可为空,相当于 <T : Any?>
fun <T> nullableMethod(param: T) {
}
fun main() {
// 完全可以传入一个 null
nullableMethod(null)
}
如果方法不想接收 null,必须手动指定泛型上界为 Any:
kotlin
// 指定上界为 Any,泛型 T 不可为空
fun <T : Any> nonNullMethod(param: T) {
}
fun main() {
nonNullMethod(null) // 报错
}
星投影 (*) ------ "我不在乎里面装了啥"
我们经常能看到 List<*> 的写法,这是什么意思呢?
其中的 * 是星投影符,等价于 Java 的 ? 通配符。它用于完全不关心容器里具体装了什么类型的场景 。我们可以读取里面的元素(取到的最多是 Any? 类型),但不能写入。
比如,一个打印任意 List 长度的方法就可以使用星投影:
kotlin
// 使用 List<*> 告诉编译器:
// 我不在乎这个列表的具体泛型类型,我只调用与具体类型无关的 API,换个说法,只调用 List 本身的 API,比如获取 size
fun printListSize(list: List<*>) {
println("这个列表有 ${list.size} 个元素")
}
fun main() {
val stringList = listOf("A", "B", "C")
val intList = listOf(1, 2, 3, 4)
printListSize(stringList)
printListSize(intList)
}
你可能会想到:为什么不使用 Any 呢?比如用 MutableList<Any>?
即使 MutableList<Any> 代表了能装任意对象的列表,但如果把 MutableList<String> 当作 MutableList<Any> 来用是不安全的。
如果允许这么做,你会发现我们竟然可以往字符串列表中,塞入一个整形 100。
那为什么 Kotlin 中不可变的列表 List<String> 却可以赋值给 List<Any>?泛型运行时又是如何工作的?
这涉及到了 Kotlin 泛型的进阶概念:协变 (Covariance) 、逆变 (Contravariance) 以及类型擦除 (Type Erasure)。我会在下一篇博客中彻底揭晓。