一、泛型是啥?为啥需要它
-
问题: 没有泛型之前,比如Java的
ArrayList,它可以装任何Object。你放个String进去,取出来时编译器不知道它原来是String,你需要手动强转 ((String) myList.get(0))。容易出错(运行时可能ClassCastException),代码也不优雅。 -
解决方案 - 泛型: 在定义 类、接口或方法时,用一个占位符 (比如
<T>,<E>,<K, V>)来表示类型。在使用时,再用具体的类型 (比如String,Int,User)替换这个占位符。 -
核心好处:
- 类型安全: 编译器帮你检查放进去的东西和取出来使用的东西类型是否匹配,把运行时错误提前到编译时。
- 代码重用: 写一套逻辑(比如一个列表操作),就能适用于多种数据类型,不用为每种类型都重写一遍。
- 代码清晰: 一看
List<String>就知道里面全是字符串,代码意图更明确。
二、基础用法
1. 泛型类 / 接口 (定义贴了标签的盒子)
- 语法:
class ClassName<T> { ... }或interface InterfaceName<T> { ... } - 例子: 自己做个万能盒子
Box
kotlin
// Kotlin
class Box<T>(var content: T) { // T 是类型参数,代表"某种类型"
fun getContent(): T = content
fun setContent(newContent: T) {
content = newContent
}
}
// 使用 (告诉编译器 T 现在是 String)
val stringBox: Box<String> = Box("Hello") // 只能放String
val content: String = stringBox.getContent() // 取出来直接是String,不用强转!
val intBox: Box<Int> = Box(42) // 只能放Int
val number: Int = intBox.getContent() // 取出来直接是Int
- Android常见例子:
LiveData<T>,RecyclerView.Adapter<VH : ViewHolder>,Repository<T>
2. 泛型函数 (定义能处理多种类型物品的操作)
- 语法:
fun <T> functionName(param: T): T { ... }(函数名前的<T>声明类型参数) - 例子: 一个打印任何类型内容的函数
kotlin
// Kotlin
fun <T> logItem(item: T) { // T 可以是任何类型
println("Item: $item")
}
logItem("Kotlin is cool!") // T 是 String
logItem(3.14159) // T 是 Double
logItem(User("Alice")) // T 是 User
- Android常见例子:
Gson().fromJson(jsonString, T::class.java), 很多工具类函数如listOf<T>(...),mapOf<K, V>(...)
3. 泛型约束 (给标签加限制,比如"只能放食物")
- 问题: 有时候你的盒子或操作不是对所有类型都有效,比如只能处理"可比较"的东西。
- 解决方案: 使用
:指定上界 (upper bound)。
kotlin
// Kotlin: 确保 T 实现了 Comparable 接口,这样才能比较
fun <T : Comparable<T>> max(a: T, b: T): T { // T 必须是 Comparable<T> 或其子类型
return if (a > b) a else b
}
val bigger = max(10, 5) // Int 实现了 Comparable
val longer = max("apple", "zebra") // String 实现了 Comparable
// max(Box(1), Box(2)) // 错误!Box 没有实现 Comparable
- 多重约束 (Kotlin特有): 用
where子句
kotlin
interface Eatable
interface Drinkable
fun <T> consumeItem(item: T) where T : Eatable, T : Drinkable {
item.eat()
item.drink()
}
- Android常见例子: 要求类型是
Activity或Fragment的子类,或者要求实现某个特定接口(如Parcelable)。
三、Kotlin泛型的超能力
Java泛型有个大问题叫类型擦除 :编译器在编译后会去掉泛型类型信息(Box<String> 在运行时变成 Box<Object>)。这导致:
- 你无法在运行时直接知道
T具体是什么类型 (if (something is T)不行)。 - 你不能直接拿
T::class。 - 处理数组和某些反射时比较麻烦。
Kotlin 提供了强大的工具来解决这些问题:
1. 声明处型变 (out 和 in) - 协变(covariant)与逆变(contravariant)
这是Kotlin最优雅的特性之一,让你在定义类/接口时就规定好类型参数的"安全流向"。
-
理解"型变": 想象父子类关系。
Dog是Animal的子类。一个装Dog的盒子 (Box<Dog>) 和一个装Animal的盒子 (Box<Animal>) 之间是什么关系?-
不变 (
Invariant): 默认情况。Box<Dog>和Box<Animal>没有任何关系 。不能把Box<Dog>赋值给Box<Animal>的引用,反之也不行。因为万一Box<Animal>引用指向了Box<Dog>,你往里放个Cat(也是Animal) 就炸了。 -
协变 (
Covariant) -out(生产者): 如果类只生产T(作为返回值),不消费T(不作为参数),那么Box<Dog>可以当作Box<Animal>来用。因为从Box<Dog>里取出来的是Dog,它肯定是Animal,安全。- 语法:
class Box<out T>(val content: T) { fun get(): T { ... } }(val保证了只读,天然安全) - 例子: Kotlin 的
List<out T>。你只能从List里取元素 (get),不能往里加元素 (add)。所以List<Dog>是List<Animal>的子类型。
kotlinfun feedAnimals(animals: List<Animal>) { ... } val dogs: List<Dog> = listOf(Dog(), Dog()) feedAnimals(dogs) // ✅ 安全!因为 feedAnimals 只会从列表里读 Animal, Dog 是 Animal - 语法:
-
逆变 (
Contravariant) -in(消费者): 如果类只消费T(作为参数),不生产T(不作为返回值),那么Box<Animal>可以当作Box<Dog>来用。因为Box<Animal>能处理任何Animal,当然也能处理它的子类Dog。- 语法:
interface Consumer<in T> { fun consume(item: T) } - 例子: Kotlin 的
Comparable<in T>。Comparable<Animal>可以比较任何Animal。那么一个Dog对象如果实现了Comparable<Animal>,它就一定能和其他Dog比较(因为Dog是Animal),所以Comparable<Animal>可以当作Comparable<Dog>来用。
kotlinfun sortDogs(dogs: List<Dog>, comparator: Comparator<in Dog>) { ... } val animalComparator: Comparator<Animal> = ... // 可以比较任何 Animal sortDogs(dogs, animalComparator) // ✅ 安全!animalComparator 能比较 Dog (Animal 的子类) - 语法:
-
-
总结
out和in:out T:T只出现在函数的返回位置 (生产T)。使类型协变 (C<Sub><:C<Super>)。(List<out T>是生产者)in T:T只出现在函数的参数位置 (消费T)。使类型逆变 (C<Super><:C<Sub>)。(Consumer<in T>是消费者)
2. 类型投影 (使用处型变) - 临时放宽限制
有时候你拿到的泛型类本身没有用 out/in 声明(比如Java的类,或者设计时没考虑),但你在某个特定使用的地方 知道它是安全的。这时可以用类型投影来临时告诉编译器。
out投影 (? extends Tin Java,<out T>in Kotlin): 相当于临时把这个类型当作只读的(生产者)。in投影 (? super Tin Java,<in T>in Kotlin): 相当于临时把这个类型当作只写的(消费者)。
kotlin
// 假设有个 Java 类:public class JavaBox<T> { void put(T item); T get(); }
val javaBox: JavaBox<Animal> = JavaBox(Dog())
// Kotlin 中使用,不知道 JavaBox 是否安全
fun copyFrom(source: JavaBox<out Animal>, destination: JavaBox<in Animal>) {
val item: Animal = source.get() // ✅ 安全,从 out 投影的 source 取,得到 Animal
destination.put(item) // ✅ 安全,向 in 投影的 destination 放 Animal
// source.put(Animal()) // ❌ 错误!source 是 out 投影,不能 put (消费)
// val dog: Dog = source.get() // ❌ 错误!source.get() 返回的是 Animal,不一定是 Dog
}
copyFrom(javaBox, anotherAnimalBox) // 使用
3. 星号投影 (*) - "我不知道具体类型,但知道点限制"
当你对泛型类型参数一无所知,但又不关心具体类型,或者只想用一些由约束带来的安全操作时使用。它综合了 out Any? 和 in Nothing 的特性。
List<*>:相当于List<out Any?>。我知道这是一个列表,里面装着某种特定的类型(但我不知道是哪种),我可以安全地读取 元素(作为Any?),但不能写入元素(因为不知道具体类型)。MutableList<*>:相当于MutableList<out Any?>。同上,只能读不能写。Consumer<*>:相当于Consumer<in Nothing>。我知道这是一个消费者,它消费某种特定的类型(但我不知道是哪种)。我不能调用 带T参数的consume方法(因为没有任何东西是Nothing的子类),但可以调用其他不涉及T的方法。
kotlin
fun printSize(list: List<*>) {
println(list.size) // 可以,size 不依赖具体类型
for (item in list) { // item 类型是 Any?
println(item)
}
// list.add("anything") // ❌ 错误!不知道具体类型,不能添加
}
4. 具体化的类型参数 (reified) - 突破类型擦除的魔法!
这是Kotlin解决"无法在运行时知道 T 是什么类型"这个痛点的终极武器。需要配合 inline 函数使用。
- 原理:
inline函数会把函数体代码直接"粘贴"到调用处。编译器在"粘贴"的时候,知道调用时传入的具体类型是什么,所以它可以用真实的类型替换掉reified T。 - 语法:
inline fun <reified T> functionName(...) { ... } - 用途: 最常见的场景是做类型检查和获取
Class对象。
kotlin
// 传统方式 (繁琐,需要显式传递 Class 对象)
fun <T> parseJson(json: String, clazz: Class<T>): T {
return Gson().fromJson(json, clazz)
}
val user = parseJson(jsonString, User::class.java)
// 使用 reified (优雅!)
inline fun <reified T> parseJson(json: String): T {
return Gson().fromJson(json, T::class.java) // 直接拿到 T 的 Class 对象!
}
val user = parseJson<User>(jsonString) // 编译器知道 T 是 User,粘贴代码时用 User::class.java 替换 T::class.java
- 另一个例子: 检查类型
kotlin
inline fun <reified T> isInstanceOf(item: Any): Boolean {
return item is T // ✅ 现在可以了!因为内联后,T 是具体的类型
}
if (isInstanceOf<String>(someObject)) {
println("It's a String!")
}
- 限制:
reified类型参数只能用在inline函数中。不能用于属性、非内联函数、类的类型参数等。
四、高级应用 & 解决难点
-
通用数据仓库 (Repository):
kotlininterface Repository<out T : Identifiable> { // T 协变,且必须是 Identifiable 子类 fun getById(id: String): T? fun getAll(): List<T> } class UserRepository : Repository<User> { ... } class ProductRepository : Repository<Product> { ... } // 使用 val userRepo: Repository<User> = UserRepository() val user: User? = userRepo.getById("123") -
泛型事件处理器 (Event Bus / Callbacks): 使用
in处理逆变。kotlininterface EventListener<in T> { fun onEvent(event: T) } class ClickEvent class NetworkEvent class MyClickListener : EventListener<ClickEvent> { override fun onEvent(event: ClickEvent) { ... } } // 一个能处理任何事件的监听器 val universalListener = object : EventListener<Any> { override fun onEvent(event: Any) { ... } } fun registerClickListener(listener: EventListener<ClickEvent>) { ... } registerClickListener(MyClickListener()) // ✅ 正常 registerClickListener(universalListener) // ✅ 安全!因为 EventListener<Any> 是 EventListener<ClickEvent> 的子类型 (in 逆变) // universalListener 能处理 Any, 当然也能处理 ClickEvent (Any 的子类) -
带类型安全的 Builder / DSL:
kotlinclass QueryBuilder<T> { private var conditions: MutableList<String> = mutableListOf() fun where(condition: String): QueryBuilder<T> { conditions.add(condition) return this } fun build(): Query<T> { ... } } fun <T> select(columns: String): QueryBuilder<T> = QueryBuilder<T>() // 使用链式调用,类型 T 贯穿始终 val userQuery: Query<User> = select<User>("id, name") .where("age > 21") .build() -
解决
reified无法用于属性的限制: 如果需要在类内部保存类型信息,可以传递KClass<T>或者在companion object中使用reified初始化。kotlinclass MyTypeSafeHolder<T>(private val clazz: KClass<T>) { private var value: T? = null fun setValue(newValue: T) { ... } fun getValue(): T? { ... } fun isValueOfType(item: Any): Boolean = clazz.isInstance(item) } // 或者利用伴生对象初始化 class MyReifiedClass { companion object { inline fun <reified T> createHolder(): MyTypeSafeHolder<T> { return MyTypeSafeHolder(T::class) } } } val holder = MyReifiedClass.createHolder<String>() // 内部持有 String::class
五、总结 & 关键点
-
核心价值: 类型安全 + 代码重用 + 表达力强。
-
基础构件: 泛型类(
class Box<T>)、泛型接口、泛型函数(fun <T> doSomething(item: T))。 -
Kotlin 利器:
out(协变): 生产者,只读倾向。C<Sub>可当C<Super>用。in(逆变): 消费者,只写倾向。C<Super>可当C<Sub>用。reified+inline: 运行时获取类型参数,告别Class参数传递,简化反射和反序列化。- 类型投影 (
<out T>,<in T>,*): 安全地处理外来泛型对象。
-
设计原则:
- 优先考虑在声明处使用
out/in。 - 如果类既生产又消费
T,保持不变 (invariant) 是最安全的。 - 善用泛型约束 (
<T : SomeType>,where T : A, T: B) 确保操作有效。 reified是解决特定运行时类型需求的强大工具,但仅限于inline函数。
- 优先考虑在声明处使用
理解泛型的关键在于多练习,多思考类型参数的"来源"和"去向"(生产还是消费),并善用Kotlin提供的 out/in/reified 等特性来编写更安全、更优雅、更灵活的代码。