深度解析Kotlin泛型:从基础到实战

1 为什么需要泛型?

泛型的核心目的只有一个:在编译期提供类型安全。并消除强制类型转换带来的冗余代码和潜在风险。

想象一下,如果没有泛型:

csharp 复制代码
//没有泛型,使用Any
val list = ArrayList<Any>()
list.add("hello")
list.add(123)

val str = list[0] as String //必须强制转型,且容易运行出错

使用泛型后,编译器会自动帮我们检查类型

java 复制代码
val list = ArrayList<String>()
        list.add("hello")
//      list.add(123) 编译错误,类型不匹配

        val str = list[0] //自动推断为String,无需转型

2 Kotlin泛型基础

2.1声明泛型类/接口

泛型可以用于类,接口,方法。

kotlin 复制代码
//泛型类
class Box<T>(t: T) {
    var value = t
}

//泛型接口

interface Repository<T> {
    fun getT(id: Int): T
    fun save(entity: T)
}

//泛型方法
fun <T> singletonList(item: T): List<T> {
    return listOf(item)
}

//调用时,类型推断通常可以自动完成
val box=Box("Hello") //推断出Box<String>
val list= singletonList(123) //推断出List<Int>

3 核心难点:型变

Kotlin为了解决Java泛型中的通配符(? extends T和? super T)使用不便的问题,引入了声明处型变和类型投影。

3.1为什么需要型变?

考虑一个简单的问题:List是不是List的子类型? 直觉上,String是Any的子类,那么List也应该是List的子类。但在默认情况下,Kotlin和Java一样,泛型是不可变的。

kotlin 复制代码
fun addItem(list: MutableList<Any>) {
    list.add(123) //可以添加Int类型
}

val strings= mutableListOf("a","b","c")
//addItem(strings) 编译错误!类型不匹配

为什么不允许?因为如果允许,addItem就会向strings列表中添加Int,导致类型混乱。为了类型安全,默认泛型是不可变的。

为了在保证安全的前提下提供灵活性,Kotlin引入了out(协变)和in(逆变)。

3.2 协变-out

概念:如果 AB 的子类型,那么 Producer<A> 也是 Producer<B> 的子类型。这叫做协变。

使用场景:当泛型类型只作为输出(生产者),不作为输入(消费者)时,这叫做协变。

Kotlin的list接口是只读的,它被声明为out:

这意味着:

kotlin 复制代码
fun printAll(list: List<Any>) {
    for (item in list) println(item)
}

val stringList: List<String> = listOf("a", "b")
//printAll(stringList)  合法 List<String> 是List<Any>的子类型

定义自己的协变类:

kotlin 复制代码
class Producer<out T>(private val value: T) {
    //只能将T用于out位置(返回值)
    fun get(): T {
        return value
    }

    //不能将T用于in位置(参数)
//    fun set(item:T){ //编译错误
//
//    }
}

fun main() {
    val producerString: Producer<String> = Producer("Hello")
    val producerAny: Producer<Any> = producerString //赋值成功,协变。可以把子类对象赋值给父类引用
    println(producerAny.get())
}

3.3 逆变 概念:如果 AB 的子类型,那么 Consumer<B>Consumer<A> 的子类型。这叫做逆变。 使用场景:当泛型类型只作为输入(消费者),不作为输出(生产者)时,可以使用in。

Kotlin的Comparable接口被声明为in:

kotlin 复制代码
class Consumer<in T> {
    fun consume(item: T) {
        println("Consuming $item")
    }
    //不能将T用于out位置 (返回值)
    //fun produce():T{} //编译错误
}


fun main() {
    val consumerAny:Consumer<Any> = Consumer()
    val consumerString:Consumer<String> = consumerAny //赋值成功,逆变
    consumerString.consume("Hello") //安全

}

3.4 总结对比

修饰符 名称 子类型关系 使用位置 类比 Java
不变 (Invariant) MutableList<String>MutableList<Any> 无关 可读可写 MutableList<T>
out 协变 (Covariant) Producer<A>Producer<B> 的子类 (如果 A <: B) 只能生产 (返回) ? extends T
in 逆变 (Contravariant) Consumer<B>Consumer<A> 的子类 (如果 A <: B) 只能消费 (接收) ? super T

3.5 星投影

当你不知道或不关心泛型的具体类型,可以使用*

  • List<*>:表示包含某种类型元素的列表,但具体类型未知。等价于 List<out Any?>。你可以从中读取 Any?,但不能写入任何东西(除了 null)。
  • MutableList<*>:等价于 MutableList<out Any?>。不能写入,因为不知道具体类型。
  • Function<*, String>:第一个参数类型未知,第二个参数固定为 String

4 实战技巧与最佳实践

4.1 使用reified具体化泛型

在JVM上,泛型在运行时会被擦除。Kotlin提供了reified关键字,结合inline函数,可以在运行时保留泛型信息。

kotlin 复制代码
inline fun <reified T> isInstance(value: Any): Boolean = value is T

fun main() {
   println(isInstance<String>("Hello")) //true
   println(isInstance<Int>("Hello")) //false

}

inline fun <reified T : Activity> Context.startActivityExt() {
   val intent = Intent(this, T::class.java)
   startActivity(intent)

}

class TestActivity : ComponentActivity() {
   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(R.layout.recyclerview_demo)
       startActivityExt<TestActivity>()
   }
}

5 总结

Kotlin的泛型系统在Java的基础上进行了大幅优化: 1 默认不可变保证了类型安全。 2 声明处型变(out/in)让类的设计者可以明确定义型变关系。 3 类型投影(星投影)提供了使用处的灵活性。 4 reified泛型结合inline函数,解决了JVM的类型擦除问题,让泛型更加强大。

理解这些概念,不仅能让你写出更健壮的代码,也能让你在使用Kotlin协程,Jetpack Compose等现代框架时,更加得心应手。

相关推荐
luanma1509802 小时前
Laravel3.x:PHP框架的里程碑
android
Kapaseker2 小时前
Kotlin 精讲 — companion object
android·kotlin
studyForMokey3 小时前
【Android面试】打包 & 启动专题
android·面试·职场和发展
zh_xuan3 小时前
Android Jetpack 使用Room数据库
android·android jetpack·room
斯密码赛我是美女3 小时前
周报--2
android·数据库
zhangren024683 小时前
Laravel9.x核心特性全解析
android
耶叶4 小时前
Android 开发:基于Scaffold的电子邮件App
android·android-studio
三少爷的鞋4 小时前
是时候告别业务层 Manager 了:Android 架构升级到 UseCase + Repository
android