3、泛型类与接口:声明泛型类、泛型接口、泛型约束

泛型这东西,说白了就是「类型占位符」。

我刚开始学 Kotlin 时,觉得泛型不就是 Java 那套吗?后来写多了才发现,Kotlin 的泛型设计更严谨,也更灵活。尤其是泛型约束,用好了能让代码既安全又优雅。

3.1 声明泛型类

先看一个最简单的例子。假设我们要做一个盒子,能装任何类型的东西。

class Box<T>(val item: T)

fun main() {
    val stringBox = Box("Hello")
    val intBox = Box(42)
}

这里的 T 就是类型参数。你传什么类型,它就变成什么类型。

我在项目中遇到过这样的场景:需要封装一个网络请求的结果,可能是成功的数据,也可能是错误信息。用泛型类就很合适。

sealed class Result<out T> {
    data class Success<T>(val data: T) : Result<T>()
    data class Error(val message: String) : Result<Nothing>()
}

嗯,这里用了 out 关键字,后面会讲到协变。先记住这个模式就行。

3.2 声明泛型接口

泛型接口和泛型类类似。比如定义一个仓库接口,能存能取。

interface Repository<T> {
    fun save(item: T)
    fun findById(id: String): T?
}

实现的时候,你可以指定具体类型:

class UserRepository : Repository<User> {
    private val users = mutableMapOf<String, User>()

    override fun save(item: User) {
        users[item.id] = item
    }

    override fun findById(id: String): User? {
        return users[id]
    }
}

你想想看,如果没有泛型,你得为每种类型写一个接口,那得多累?

我的习惯:定义接口时尽量用泛型,哪怕当前只有一个实现。这样后续扩展起来,改动的成本几乎为零。

3.3 泛型约束

泛型约束,就是限制类型参数的范围。说白了,你不能什么类型都往里塞。

3.3.1 上界约束

最常见的约束是上界约束,用 : 指定。

class NumberBox<T : Number>(val item: T)

fun main() {
    val intBox = NumberBox(10)      // 可以
    val doubleBox = NumberBox(3.14) // 可以
    // val stringBox = NumberBox("Hi") // 编译错误!
}

这里 T : Number 表示 T 必须是 Number 的子类。String 不是 Number 的子类,所以编译不通过。

我曾经在项目中犯过一个错:没有加约束,结果运行时类型转换炸了。从那以后,我写泛型必加约束,哪怕只是 Any

3.3.2 多个上界

有时候一个约束不够。比如你要 T 既是 Comparable 又是 Serializable。

class MultiBound<T>(val item: T) where T : Comparable<T>, T : Serializable

语法有点特别:where 关键字后面跟多个约束,用逗号隔开。

我个人习惯把 where 放在类名后面,这样看起来更清晰:

class MultiBound<T>(val item: T) 
    where T : Comparable<T>, 
          T : Serializable

3.3.3 可空类型约束

默认情况下,泛型参数是可空的。如果你不想让它为空,可以加 Any 约束。

class NonNullBox<T : Any>(val item: T)

fun main() {
    val box = NonNullBox("Hello") // 可以
    // val nullBox = NonNullBox(null) // 编译错误!
}

这个细节很容易被忽略。我见过不少同事写的泛型类,传 null 进去直接崩了。加个 : Any 就能避免。

注意:Kotlin 的泛型是编译时擦除的。运行时你拿不到 T 的具体类型。如果需要运行时类型信息,得用 reified 关键字,这个后面会讲。

3.4 实战:泛型约束的应用

来看一个实际例子。假设我们要写一个通用的排序工具,只对实现了 Comparable 的类型生效。

fun <T : Comparable<T>> List<T>.sortedDescending(): List<T> {
    return this.sortedByDescending { it }
}

fun main() {
    val numbers = listOf(3, 1, 4, 1, 5, 9)
    println(numbers.sortedDescending()) // [9, 5, 4, 3, 1, 1]
}

这里 T : Comparable<T> 保证了只有可比较的类型才能调用这个方法。如果你传一个没有实现 Comparable 的类,编译直接报错。

你想想看,这比运行时抛异常强多了吧?

3.5 泛型约束与类型安全

泛型约束的核心价值,就是让编译器帮你检查类型。我总结了一个原则:

能用泛型约束解决的问题,绝不留到运行时。

举个例子,假设你要写一个函数,只接受数字类型的集合:

fun <T : Number> sum(list: List<T>): Double {
    return list.sumOf { it.toDouble() }
}

fun main() {
    println(sum(listOf(1, 2, 3)))       // 6.0
    // println(sum(listOf("a", "b")))   // 编译错误!
}

没有泛型约束的话,你可能会写 List<Any>,然后在函数内部做类型检查。那样既啰嗦又不安全。

我曾经接手过一个老项目,里面全是 List<Any>as? Type 的转换。改起来那叫一个痛苦。所以我现在写新代码,泛型约束能加就加,绝不偷懒。

3.6 小结

这一章我们聊了:

  • 泛型类:用 <T> 声明类型参数
  • 泛型接口:接口也能带类型参数,实现时指定具体类型
  • 泛型约束:用 :where 限制类型范围
  • 实战应用:约束让代码更安全,错误更早暴露

下一章我们会深入泛型的高级特性——协变与逆变。到时候你会发现,Kotlin 的泛型设计比 Java 优雅得多。

嗯,今天就到这里。记得多写多练,泛型这东西,光看是学不会的。