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 做得更好。
嗯,先消化这些吧。写代码的时候多想想:这个类型能不能用泛型替代? 养成习惯后,你的代码质量会上一个台阶。