一、泛型是啥?为啥需要它
-
问题: 没有泛型之前,比如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 T
in Java,<out T>
in Kotlin): 相当于临时把这个类型当作只读的(生产者)。in
投影 (? super T
in 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
等特性来编写更安全、更优雅、更灵活的代码。