深入浅出 Android 泛型的使用

一、泛型是啥?为啥需要它

  • 问题: 没有泛型之前,比如Java的ArrayList,它可以装任何Object。你放个String进去,取出来时编译器不知道它原来是String,你需要手动强转 ((String) myList.get(0))。容易出错(运行时可能ClassCastException),代码也不优雅。

  • 解决方案 - 泛型:定义 类、接口或方法时,用一个占位符 (比如 <T>, <E>, <K, V>)来表示类型。在使用时,再用具体的类型 (比如 String, Int, User)替换这个占位符。

  • 核心好处:

    1. 类型安全: 编译器帮你检查放进去的东西和取出来使用的东西类型是否匹配,把运行时错误提前到编译时。
    2. 代码重用: 写一套逻辑(比如一个列表操作),就能适用于多种数据类型,不用为每种类型都重写一遍。
    3. 代码清晰: 一看 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常见例子: 要求类型是 ActivityFragment 的子类,或者要求实现某个特定接口(如 Parcelable)。

三、Kotlin泛型的超能力

Java泛型有个大问题叫类型擦除 :编译器在编译后会去掉泛型类型信息(Box<String> 在运行时变成 Box<Object>)。这导致:

  1. 你无法在运行时直接知道 T 具体是什么类型 (if (something is T) 不行)。
  2. 你不能直接拿 T::class
  3. 处理数组和某些反射时比较麻烦。

Kotlin 提供了强大的工具来解决这些问题:

1. 声明处型变 (outin) - 协变(covariant)与逆变(contravariant)

这是Kotlin最优雅的特性之一,让你在定义类/接口时就规定好类型参数的"安全流向"。

  • 理解"型变": 想象父子类关系。DogAnimal 的子类。一个装 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> 的子类型。
      kotlin 复制代码
      fun 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 比较(因为 DogAnimal),所以 Comparable<Animal> 可以当作 Comparable<Dog> 来用。
      kotlin 复制代码
      fun sortDogs(dogs: List<Dog>, comparator: Comparator<in Dog>) { ... }
      val animalComparator: Comparator<Animal> = ... // 可以比较任何 Animal
      sortDogs(dogs, animalComparator) // ✅ 安全!animalComparator 能比较 Dog (Animal 的子类)
  • 总结 outin

    • out TT 只出现在函数的返回位置 (生产 T)。使类型协变 (C<Sub> <: C<Super>)。(List<out T> 是生产者)
    • in TT 只出现在函数的参数位置 (消费 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 函数中。不能用于属性、非内联函数、类的类型参数等。

四、高级应用 & 解决难点

  1. 通用数据仓库 (Repository):

    kotlin 复制代码
    interface 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")
  2. 泛型事件处理器 (Event Bus / Callbacks): 使用 in 处理逆变。

    kotlin 复制代码
    interface 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 的子类)
  3. 带类型安全的 Builder / DSL:

    kotlin 复制代码
    class 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()
  4. 解决 reified 无法用于属性的限制: 如果需要在类内部保存类型信息,可以传递 KClass<T> 或者在 companion object 中使用 reified 初始化。

    kotlin 复制代码
    class 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

五、总结 & 关键点

  1. 核心价值: 类型安全 + 代码重用 + 表达力强。

  2. 基础构件: 泛型类(class Box<T>)、泛型接口、泛型函数(fun <T> doSomething(item: T))。

  3. Kotlin 利器:

    • out (协变): 生产者,只读倾向。C<Sub> 可当 C<Super> 用。
    • in (逆变): 消费者,只写倾向。C<Super> 可当 C<Sub> 用。
    • reified + inline 运行时获取类型参数,告别 Class 参数传递,简化反射和反序列化。
    • 类型投影 (<out T>, <in T>, *): 安全地处理外来泛型对象。
  4. 设计原则:

    • 优先考虑在声明处使用 out/in
    • 如果类既生产又消费 T,保持不变 (invariant) 是最安全的。
    • 善用泛型约束 (<T : SomeType>, where T : A, T: B) 确保操作有效。
    • reified 是解决特定运行时类型需求的强大工具,但仅限于 inline 函数。

理解泛型的关键在于多练习,多思考类型参数的"来源"和"去向"(生产还是消费),并善用Kotlin提供的 out/in/reified 等特性来编写更安全、更优雅、更灵活的代码。

相关推荐
moning几秒前
realStartActivity 是由哪里触发的?
前端
德莱厄斯11 分钟前
简单聊聊小程序、uniapp及其生态圈
前端·微信小程序·uni-app
tianchang14 分钟前
从输入 URL 到页面渲染:浏览器做了什么?
前端·面试
Spider_Man16 分钟前
还在被“回调地狱”折磨?Promise让你的异步代码优雅飞升!
前端·javascript
tq108617 分钟前
值类:Kotlin中的零成本抽象
java·linux·前端
怪兽_18 分钟前
CSS实现简单的音频播放动画
前端
墨夏1 小时前
TS 高级类型
前端·typescript
程序猿师兄1 小时前
若依框架前端调用后台服务报跨域错误
前端
前端小巷子1 小时前
跨标签页通信(三):Web Storage
前端·面试·浏览器
工呈士1 小时前
TCP 三次握手与四次挥手详解
前端·后端·面试