泛型是在开发中非常重要的一种特性,可以帮助我们在不同类型之间复用相似的逻辑代码。包括:泛型基础使用、型变、星投影三大部分,泛型是对程序的抽象
泛型的基础很简单,:
表示了这个泛型类继承了TV
型变
泛型是不变的,而型变就是为了解决泛型的不变性
型变讨论的基础在于子类和父类:已知Cat
是 Animal
的子类,MutableList<Animal>
和 MutableList<Cat>
编译器会认为这两个类是没有任何关系的,也就是说泛型是不变的
//父类
open class Animal {}
//子类
class Cat : Animal()
//子类
class Dog : Animal()
父类集合,传入子类集合(实际情况编辑器会报错)
假设使用MutableList
来代替MutableList ,编译器不会阻止报错,那么会发生什么呢? ```kotlin fun foo(list:MutableList ){ //list实际上是 -> MutableList 不能存储Dog list.add(Dog()) //① 出错 val animal:Animal = list[0] //实际上取出来的是Cat }
fun main() {
//需要传递MutableList
上述代码中,list是需要Animal的集合,如果传入Cat集合,往list存入Dog其他的动物这样显然是不正确的
2. 子类集合,传入父类集合
假设使用MutableList<Animal>来代替MutableList<Cat>,假如编译器不会阻止报错,那么会发生什么呢?
```kotlin
fun foo2(list:MutableList<Cat>){
list.add(Cat())
//① 出错
val car:Cat = list[0] //假设编辑器通过,那么第一条数据,实际上取出来的是Animal对象而不是Cat对象
}
fun main() {
foo2(mutableListOf<Animal>(Animal())) //编辑器会提示报错,假设这段代码通过
}
上述代码中,list是需要Cat集合,如果传入Animal集合,list内部正常的add存储是没有问题的,但是取出第一条数据不是Cat对象,而是Animal对象。
所以在默认情况下,编译器会任务MutableList
和MutableList 之间不存在继承关系,它们之间无法替换,所以如果写上面的两种代码,编译器会提示报错,这就是泛型的不变性
泛型的逆变和协变就可以登场了
逆变
父子关系发生了颠倒就是泛型的逆变(父类泛型,代替子类泛型,父子关系发生颠倒),又分为使用处逆变和声明处的逆变
open class TV {
}
class xiaomiTV : TV() {
}
fun foo(tv: TV) {
}
//class Controller<in T> 声明处型变
class Controller<in T> {
fun turnOn(tv: T) {
}
}
//controller: Controller<in xiaomiTV> 使用处型变
fun buy(controller: Controller<in xiaomiTV>) {
val xiaomiTV = xiaomiTV()
//打开小米电视机
controller.turnOn(xiaomiTV)
}
fun main() {
//xiaomiTV 是TV的子类
foo(xiaomiTV())
//万能遥控器 TV 是所有电视机的父类
val controller = Controller<TV>()
// Controller<xiaomiTV> 和 Controller<TV>()
//添加了in 关键字,使用TV 代替了 xiaomiTV,也就是说TV是xiaomiTV的子类 万能的遥控器成了小米遥控器的子类了
//父子关系发生了颠倒,这就是泛型的逆变,逆变有分为使用处逆变和声明处逆变
buy(controller)
}
协变
父子关系一致的现象(泛型默认是不变的,也就是父类和子类的泛型认为是没有关系的,而协变可以让泛型具备父子关系),就是泛型的协变
open class Food {}
//KFC 是 Food的子类
class KFC : Food() {}
//饭店的角色
class Restaurant<out T> {
fun orderFood(): T? {
return null
}
}
//点外卖的方法,找到饭店,<Food> 泛型为Food 食物类型即可
fun orderFood(restaurant: Restaurant<out Food>){
val food = restaurant.orderFood()
}
fun main(){
//点击KFC
val kfc = Restaurant<KFC>()
//如果要使用Restaurant<KFC> 来代替 Restaurant<Food> ,Restaurant<KFC>看作是 Restaurant<Food>的子类
orderFood(kfc)//报错了 因为类型不匹配,Restaurant<Food> 和 Restaurant<KFC> 因为泛型是不变的 编译器认为这两个类没有任何关系
}
型变的使用场景
那么什么时候使用协变和逆变呢:Consumer in, Producer out ! 消费者 in,生产者 out
传入 in,传出 out。或者也可以说:泛型作为参数的时候,用 in,泛型作为返回值的时候,用 out。
逆变
// 逆变
// ↓
public interface Comparable<in T> {
// 泛型作为参数
// ↓
public operator fun compareTo(other: T): Int
}
协变
// 协变
// ↓
public interface Iterator<out T> {
// 泛型作为返回值
// ↓
public operator fun next(): T
public operator fun hasNext(): Boolean
}
需要注意的地方:Success() 方法中,泛型T作为入参,为什么还要使用协变out呢?这是因为val 只读的属性,只有getter没有setter
sealed class Result<out R> {
// 改为var后,编译器就会立马报错
// ↓
data class Success<out T>(val data: T, val message: String = "") : Result<T>()
data class Error(val exception: Exception) : Result<Nothing>()
data class Loading(val time: Long = System.currentTimeMillis()) : Result<Nothing>()
}
也就是说,如果泛型的 T,既是函数的参数类型,又是函数的返回值类型,那么,我们就无法直接使用 in 或者 out 来修饰泛型 T。
函数传入参数的时候,并不一定就意味着写入,这时候,即使泛型 T 是作为参数类型,我们也仍然要想一些办法来用 out 修饰泛型
官方的实例代码:
// 协变
// ↓
public interface List<out E> : Collection<E> {
// 泛型作为返回值
// ↓
public operator fun get(index: Int): E
// 泛型作为参数
// ↓
override fun contains(element: @UnsafeVariance E): Boolean
// 泛型作为参数
// ↓
public fun indexOf(element: @UnsafeVariance E): Int
}
Kotlin 官方源码当中的 List,也就是这里的泛型 E,它既作为了返回值类型,又作为了参数类型。在正常情况下,如果我们用 out 修饰 E,那编译器是会报错的。但我们其实很清楚,对于 contains、indexOf 这样的方法,它们虽然以 E 作为参数类型,但本质上并没有产生写入的行为。所以,我们用 out 修饰 E 并不会带来实际的问题。所以这个时候,我们就可以通过 @UnsafeVariance
这样的注解,来让编译器忽略这个型变冲突的问题。
如下代码,注意泛型P使用in修饰逆变,P作为参数入参,泛型T用out修饰协变作为返回值类型,但是注意instance变量使用var修饰,var 有getter和setter,而T为什么不会报错呢?这是因为instance使用的是private修饰的如果删除private就是报错
//in P 逆变作为泛型参数入参,out T 协变作为泛型返回值类型
abstract class BaseSingleton<in P, out T> {
//var 修饰了 out T,var 有setter为什么不会报错呢?是因为它是private修饰的去掉private就会报错
private var instance: T? = null
protected abstract val creator: (P) -> T
fun getInstance(param: P): T =
instance ?: synchronized(this) {
instance ?: creator(param).also { instance = it }
}
}
- 泛型,是对程序的一种抽象。通过泛型,我们可以实现代码逻辑复用的目的,Kotlin 标准库当中很多源代码也都是借助泛型来实现的。
- 从型变的位置来分类的话,分为使用处型变和声明处型变。
- 从型变的父子关系来分类的话,分为逆变和协变。逆变表示父子关系颠倒了,而协变表示父子关系和原来一致。
- 型变的口诀:泛型作为参数,用 in;泛型作为返回值,用 out。
- 在特殊场景下,同时作为参数和返回值的泛型参数,我们可以用 @UnsafeVariance 来解决型变冲突。
- 星投影,就是当我们对泛型的具体类型不感兴趣的时候,直接传入一个“星号”作为泛型的实参。