1. 泛型入门:为什么需要泛型?泛型的基本概念与Hello World示例

1.1 一个让我头疼的真实场景

先讲个我自己的故事吧。

几年前,我在做一个数据解析框架。那时候刚用 Kotlin 没多久,写代码还挺「Java 思维」的。我需要一个能装任何类型数据的容器——可能是 Int、String,甚至自定义的 User 对象。

我当时的第一反应是:用 Any 呗!

于是代码长这样:

class Box(private val item: Any) {
    fun get(): Any = item
}

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

    // 取出来的时候,必须强转
    val number = intBox.get() as Int
    val text = strBox.get() as String
}

看起来挺灵活对吧?但问题很快就来了。

有一次,我不小心把 Int 放进去,取的时候当成 String 强转——编译时完全没报错,运行时直接崩了。嗯,ClassCastException,老朋友了。

我当时就在想:能不能让编译器帮我把这个错误扼杀在摇篮里?

这就是泛型要解决的核心问题。

1.2 泛型到底是什么?

说白了,泛型就是「类型参数化」

你写一个类或函数时,先不指定具体类型,留个「占位符」。等真正用的时候,再告诉它:这里用 Int,那里用 String。

这样做的好处很明显:

  • 编译期类型检查——放错类型?编译器直接报红,不用等到运行时崩溃
  • 消除强制类型转换——取出来就是你要的类型,不用手动 as 来 as 去
  • 代码复用——一套逻辑,适配各种类型

一句话总结:泛型让你写出更安全、更通用的代码。我在项目中几乎每个工具类都会用到它。

1.3 泛型的 Hello World

好,理论说完了,直接上代码。这是 Kotlin 泛型最经典的入门示例:

// 定义一个泛型类
class Box<T>(private val item: T) {
    fun get(): T = item
}

fun main() {
    // 使用泛型,指定类型为 Int
    val intBox: Box<Int> = Box(42)
    // 类型推断可以简化写法
    val strBox = Box("Hello, Generics!")

    // 取出来直接就是 Int 和 String,无需强转
    val number: Int = intBox.get()
    val text: String = strBox.get()

    println(number) // 42
    println(text)   // Hello, Generics!
}

看到区别了吗?

Box<T> 里的 T 就是类型参数。你传 Int,它就变成 Box<Int>;你传 String,它就变成 Box<String>。

如果你试图往 Box<Int> 里放一个 String:

val box: Box<Int> = Box("Oops") // 编译错误!

编译器直接拦住你。这感觉,说实话,比运行时崩溃舒服多了。

1.4 泛型函数:不只是类能用

泛型不光能用在类上,函数也可以。我个人特别喜欢这种写法,因为它让工具函数变得极其灵活。

// 泛型函数
fun <T> printItem(item: T) {
    println("Item: $item")
}

// 还可以返回泛型
fun <T> createBox(item: T): Box<T> = Box(item)

fun main() {
    printItem(100)       // Item: 100
    printItem("Kotlin")  // Item: Kotlin

    val box = createBox(3.14)
    println(box.get())   // 3.14
}

你看,一个 printItem 函数,Int、String、Double 都能处理。这就是泛型函数的魅力。

1.5 类型推断:Kotlin 的贴心之处

我记得刚学泛型时,总觉得写 Box<Int> 这种尖括号很麻烦。后来发现 Kotlin 有类型推断,大多数情况下可以省略:

val box1 = Box(42)          // 自动推断为 Box<Int>
val box2 = Box("Hello")     // 自动推断为 Box<String>
val box3 = Box(listOf(1,2)) // 自动推断为 Box<List<Int>>

编译器会根据传入的参数自动推导出类型参数。你想想看,这省了多少敲尖括号的时间。

小技巧:当你发现类型推断不准确时,可以显式指定类型。比如:val box: Box<Number> = Box(42)。我偶尔会在 API 边界处这么做,让意图更清晰。

1.6 避坑指南:我曾经踩过的坑

泛型虽好,但有几个地方容易翻车。我把自己踩过的坑分享给你:

  • 泛型类型不能直接实例化——你不能写 val item = T(),因为 T 在运行时被擦除了。我曾经想写一个通用的工厂类,结果卡在这里半天。
  • 泛型不能用于静态属性——companion object 里不能用类的泛型参数。这个限制让一些设计模式实现起来有点绕。
  • 基本类型会被自动装箱——Int 变成 Integer,List<Int> 底层是 List<Integer>。性能敏感的场景要注意。

特别注意:Kotlin 的泛型在 JVM 上存在类型擦除。也就是说,Box<Int>Box<String> 在运行时其实是同一个类。这意味着你不能用 is Box<Int> 来做类型检查。这个特性后面章节会详细讲。

1.7 什么时候该用泛型?

我个人的经验是:当你发现自己在重复写类似的代码,只是类型不同时,就该考虑泛型了。

举个常见的例子:

  • 集合类(List、Map、Set)——天生就是泛型的
  • 数据容器(Result、Either、Option)——函数式编程的基石
  • 仓库模式(Repository)——泛型让数据访问层更统一
  • 适配器模式——泛型让不同类型的适配变得优雅

说白了,只要你的逻辑和类型无关,就用泛型把它抽象出来。这样代码更干净,也更安全。

1.8 小结

这一章我们聊了:

  • 为什么需要泛型——为了类型安全和代码复用
  • 泛型的基本概念——类型参数化
  • 泛型类和泛型函数的写法
  • Kotlin 的类型推断
  • 一些常见的坑和最佳实践

下一章,我们会深入泛型的核心机制——类型擦除和实化类型参数。到时候你会明白,为什么 Kotlin 能比 Java 做得更好。

嗯,先消化这些吧。写代码的时候多想想:这个类型能不能用泛型替代? 养成习惯后,你的代码质量会上一个台阶。