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:
这意味着:
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:
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函数,可以在运行时保留泛型信息。
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等现代框架时,更加得心应手。