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

0 阅读4分钟

1 为什么需要泛型?

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

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

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

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

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

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

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

2 Kotlin泛型基础

2.1声明泛型类/接口

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

//泛型类
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一样,泛型是不可变的。

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

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

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

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

image.png

这意味着:

fun printAll(list: List<Any>) {
    for (item in list) println(item)
}

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

定义自己的协变类:

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

Kotlin的Comparable接口被声明为in:

image.png

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

image.png

4 实战技巧与最佳实践

4.1 使用reified具体化泛型

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

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等现代框架时,更加得心应手。