Kotlin 泛型基础:“万能魔法盒”的诞生

我们经常会用到 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
}

有了这个约束后,方法的泛型只能为数字类型,例如 IntFloatBigDecimal 等。

如果传入的参数是其他类型,如 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)。我会在下一篇博客中彻底揭晓。

相关推荐
小小测试开发5 小时前
RAG评测:把忠实度、检索命中率、噪声敏感度做成可回归的流水线
人工智能·数据挖掘·回归·kotlin
撩得Android一次心动6 小时前
Kotlin 语言【知识点整理3】
java·开发语言·笔记·学习·kotlin
Kapaseker8 小时前
我常用的 5 个 Kotlin 优化小技巧
android·kotlin
名剑走天下8 小时前
Kotlin_LiveData
开发语言·kotlin
JMchen1231 天前
Jetpack 内核实战(九):实战(上)——记事本首页列表 + 搜索功能开发
kotlin·android 实战·记事本 app 开发·recyclerview 优化·jetpack 综合实战
pengyu1 天前
【Kotlin 协程修仙录 · 炼虚境 · 初阶】 | 虚空造物:Channel 基础与协程间通信的管道艺术
android·前端·kotlin
小强闯江湖1 天前
ViewCompose:让原生 Android View 进入声明式时代
android·开源·kotlin
雨白1 天前
Kotlin 委托机制:搞懂“老板与秘书”模式
kotlin
pengyu2 天前
【Kotlin 协程修仙录 · 化神境 · 后阶】 | 异常天劫:Flow 异常处理的终极奥义与重试之道
android·kotlin