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)。我会在下一篇博客中彻底揭晓。

相关推荐
JMchen8 小时前
第 12 篇|项目整合与打包发布 —— 从 Demo 到可安装 APK 的完整收官指南
kotlin·android studio
Android打工仔8 小时前
不要在 Data 层随意把 Cold Flow 转换成 Hot Flow
android·架构·kotlin
杉氧9 小时前
拒绝重复造轮子:我写了一个生产级的 Kotlin 协程与 Flow 工具库(CoroutineKit)
android·kotlin·workflow
JMchen1 天前
第 9 篇|网络请求基础 —— 从 HttpURLConnection 到 OkHttp
kotlin·android studio
hai_android2 天前
Kotlin Flow combine 源码剖析:一个 Channel 如何优雅合并多条流
android·java·kotlin
敲代码的瓦龙2 天前
Jetpack?ViewModel!!!
android·开发语言·kotlin·android-studio
swithun2 天前
不用 WebView,我用 Kotlin + Compose Multiplatform 重写 Mermaid,并做了 2048 组对拍
android·开源·kotlin
JMchen2 天前
第 8 篇|本地存储技术选型 —— SP 与文件读写
kotlin·android studio
码农哈丁3 天前
从 JVM 到 Cargo:把整个 Kotlin 项目移植到 Rust 的踩坑实录
jvm·rust·kotlin
Kapaseker3 天前
写过 4000 行 ViewModel 后,我开始这样拆 Compose 页面
android·kotlin