协变和逆变
概念:
Kotlin 的协变与逆变统称为 Kotlin 的变型。变型是指泛型的基础类型与它的参数类型是如何关联的。对于普通类型来说,我们可以使用子类代替父类,因为子类包含了父类的全部内容。但是对于泛型来说,如果泛型的基础类型相同,其中一个参数类型是另外一个参数类型的子类,泛型类也不存在这种继承关系,无法直接替换使用。要解除这些限制,就需要用到协变与逆变。
先声明两个泛型接口
//未加 out
interface Production{
fun product():T
}
//未加 in
interface Consumer{
fun consume(item:T)
}
在声明三个类,继承关系:Burger—extend—>FastFood—extend—>Food
open class Food {}
open class FastFood :Food(){}
class Burger:FastFood(){}
接着声明食品工厂
class FoodShop : Production{
override fun product(): Food {
return Food()
}
}
class FastFoodShop : Production{
override fun product(): FastFood {
return FastFood()
}
}
协变( OUT )
main函数创建这两个食品工厂
fun main() {
//协变
var foodshop:Production = FoodShop()//正常编译
var fastfoodshop:Production = FastFoodShop() //报错
}
此时第4行代码相当于 Production
由于Production
Java为了解决这个问题提供了 Extends T>通配符,而在Kotlin中将这个通配符用out关键字来替代。需要在Production接口泛型T前加out关键字,表明泛型可以传入参数类及其子类
//加 out 编辑通过
interface Production{
fun product():T
}
不是所有类都可以变成协变的
概念:
只有当这个类只能生产类型 T 的值而不能消费它们时,才能变成协变的。就是说 T 的值只能作为函数返回值时,才能变成协变的。这也是为什么协变关键词叫做 out,表明它只能作为生产者对外输出。
以下代码中,t : T 接收入参的位置叫做 in 位置,表示它是函数参数,是消费者。: T 返回值输出数据 的位置叫做 out 位置,表示它是函数返回值,是生产者。
Kotlin |
举个例子:
//Apple和Orange都是Fruits的子类
open class Fruits(val name: String) {}
class Apple(val n: String, val male: String = "apple"): Fruits(n) {}
class Orange(val n: String, val female: String = "orange"): Fruits(n) {}
//先声明个泛型类,内部封装了一个私有的data属性并向外部提供set、get方法
class MyClass {
private var data: T? = null
fun set(t: T?) {
data = t
}
fun get(): T? = data
}
fun test(data:MyClass) {
data.set(Orange("橙子"))
}
定义测试主函数
可以看到test传入参数爆红,这就和最开始说的Production
所以kotlin是不允许这样去跨继承传参的,而换个角度想之所以这样写会出现类转换异常就是因为test方法中给set了一个Orange对象导致了问题,如果说MyClass在泛型T上是只读的即没有set方法那么就不会因为set一个Orange对象导致类型转换异常。
所以kotlin规定如果个泛型类或泛型接口定义out协变后,泛型T只能出现在out位置不能出现在in位置,即只能读不能写
逆变 ( IN )
逆变和协变是相反的,但其实道理是一样的
之所以第一句编译报错就是因为 ArrayList
同样的定义个泛型接口
interface MyClass {
fun show(d: T?): T?
}
fun testTwo(data:MyClassTwo){
var result = data.show(Apple("apple"))
}
定义测试主函数
可以看到调用时编译报错了,还是因为kotlin不支持直接跨继承传参。如果说编译不报错,那么继续走下去会看到形参data是Apple类型的MyClassTwo引用,result变量要求接收一个Apple类型实现的MyClassTwo对象,但是实际上实参data的show方法返回了一个Orange对象,由于它是Fruits的子类,所以data在实现show方法的时候并没有问题,但是result在接收的时候无法将Apple强转为Orange类型,这样就会发生类型转换异常。
和协变一样,我们换个角度想之所以这样写会出现类转换异常就是因为show方法要去返回一个Fruits对象导致了问题,如果说MyClassTwo在泛型T上是只写的即不允许泛型T出现在out位置上,那么就不会因为show方法返回了一个Orange对象而导致的类型转换问题。
kotlin为了实现这个只能写不能读的功能而提供了in关键字,即这个泛型T只能出现在in位置不能出现在out位置
总结
总的来说协变和逆变是java为了处理泛型的类型擦除而带入的新规则,kotlin在java的基础上用了out和in两个关键字来实现
out和in使用规则: