Kotlin中的协变和逆变功能不容易理解,这里将用最简明的方式介绍这两个功能
in位置&out位置
首先了解一个约定。一个泛型类或者泛型接口中的方法,它的参数列表是接收数据的地方,我们就把它称为in位置,而它的返回值是输出数据的地方,我们就把它称为out位置,如图所示:
引入问题
有了这个约定前提,接下来定义3个类:
1 | open class Person(val name: String, val age: Int) |
Student
和Teacher
类是Person
的子类,Person
类有name
和age
两个字段
现在:如果某个方法接收一个Person类型的参数,而我们传入了一个Student类进去,由于Student类是Person类的子类,所以这样做是合法的。
但是:如果某个方法接收一个List<Person>
类型的参数,而我们传入了一个List<Student>
类型的参数进去,这在Java中是不合法的。因为List<Student>
不是List<Person>
的子类,这样将可能存在类型转换的安全隐患
为什么会存在这种安全隐患呢?
下面来看一个例子:
1 | class SimpleData<T> { |
这里自定义一个SimpleData
泛型类,内部封装了一个泛型字段data
,set()
和get()
方法分别用来为data
字段赋值和获取data
字段的值
我们假设编程语言允许向某个接受SimepleData<Person>
参数的方法传入SimpleData<Student>
的实例,那么如下代码就是合法的:
1 | fun main() { |
我们发现这段代码有很大的问题,在main()
方法中,我们创建了一个Student
的实例,并将它封装到SimpleData<Student>
当中,然后将SimpleData<Student>
作为参数传递给handleSimepleData()
方法。但是handleSimepleData()
方法接收的是一个SimpleData<Person>
参数(这里假设可以编译通过),那么在handleSimepleData()
方法中,我们就可以创建一个Teacher
类的实例,并用它来替换SimpleData<Person>
参数中原有的数据。因为Teacher
是Person
的子类,所以这是合法的。
但是,我们又在main()方法中调用了SimpleData<Student>
的get()
方法来获取它内部封装的Student
数据,可现在SimpleData<Student>
中实际包含的却是一个Teacher
的实例,所以此时必定会产生类型转换异常
因此,Java中不允许使用这种方式来传递参数。换句话说,即使Student
是Person
的子类,SimpleData<Student>
并不是SimpleData<Person>
的子类。
泛型协变
从上面的分析中我们发现,问题的关键是我们在handleSimepleData()
方法中向SimpleData<Person>
里设置了一个Teacher
的实例
如果SimpleData
在泛型T上是只读的话,肯定就没有类型转换的安全隐患了,那么这个时候SimpleData<Student>
可不可以成为SimpleData<Person>
的子类呢?
这里就引出了泛型协变的定义了:假如定义了一个MyClass<T>
的泛型类,其中A是B的子类型,同时MyClass<A>
又是MyClass<B>
的子类型,那么我们就可以称MyClass
在T这个泛型上是协变的
要实现一个泛型类在其泛型类型的数据上是只读的这一点,就需要让MyClass<T>
类中的所有方法都不能接收T类型的参数。也就是说,T只能出现在out位置上,不能出现在in位置上
现在修改SimpleData
类的代码:
1 | class SimpleData<out T>(val data: T?) { |
这里我们对SimpleData
类进行了改造,在泛型T的声明前面加上了一个out关键字。这就意味着现在T只能出现在out位置上,而不能出现在in位置上,同时也意味着SimpleData
在泛型T上是协变的。
由于泛型T不能出现在in位置上,因此我们就不能使用setData()
方法为data参数赋值了,所以这里改成了使用构造函数的方式来赋值。由于我们在构造函数中使用了val关键字对T进行声明,所以构造函数中的泛型T仍然是只读的。另外,即使我们使用了var关键字,但是只要给它加上private
修饰符,保证这个泛型T对于外部而言是不可修改的,那么也是合法的写法
经过这样修改后,下面的代码就能完美编译而没有任何安全隐患了:
1 | fun main() { |
由于SimpleData
类已经进行了协变声明,那么SimpleData<Student>
自然就是SimpleData<Person>
的子类了,所以这里能够安全的向handleMyData()
方法中传递参数。然后在handleMyData()
方法中获取SimpleData
封装的数据,虽然这里泛型声明的是Person类型,实际获得的会是一个Student的实例,但由于Person是Student的父类,向上转型是完全安全的
泛型逆变
泛型逆变和泛型协变的定义完全相反:假如定义了一个MyClass<T>
的泛型类,其中A是B的子类型,同时MyClass<B>
又是MyClass<A>
的子类型,那么我们就可以称MyClass
在T这个泛型上是逆变的
从直观的角度来思考,逆变的规则好像挺奇怪的,原本A是B的子类型,怎么MyClass<B>
又反过来成为MyClass<A>
的子类型了呢?下面通过一个例子来说明
先定义一个Transformer接口,用于执行一些转换操作:
1 | interface Transformer<T> { |
在这个接口中的transform()
方法会将T类型的参数转换成String类型返回
实现这个接口方法:
1 | fun main() { |
首先我们在main()
函数里编写了一个Transformer<Person>
的匿名类实现,并通过transform()
方法将传入的Person对象转换成了一个”姓名+年龄“拼接的字符串。而handleTransformer()
方法接收的是一个Transformer<Student>
类型的参数,这里在handleTransformer()
方法中创建了一个Student对象,并调用transform()方法将Student对象转换成一个字符串
这段代码从安全的角度来分析是没有任何问题的,因为Student是Person的子类,使用Transformer<Person>
的匿名类实现将Student对象转换成一个字符串也是绝对安全的。但是实际上,在调用handleTransformer()
方法的时候却会提示语法错误,因为此时Transformer<Person>
并不是Transformer<Student>
的子类型
这个时候逆变就派上用场了,它即是专门处理这种情况的。现在修改Transformer接口中的代码:
1 | interface Transformer<in T> { |
这里我们在泛型T的声明前面加上了in关键字,这意味着T只能出现在in位置上,而不能出现在out位置上,同时也意味着Transformer在泛型T上是逆变的
经过这样的修改,Transformer<Person>
就是Transformer<Student>
的子类型了
思考
我们知道了协变的时候泛型T不能出现在in位置上,那么为什么逆变的时候又不能出现在out位置上呢?
为了说明这个问题,我们现在假设逆变允许让泛型T出现在out位置上的,修改Transformer中的代码:
1 | interface Transformer<in T> { |
我们将transform()方法改成了接收name和age这两个参数,并把返回值类型改成了泛型T。由于逆变是不允许泛型T出现在out位置上的,这里为了能让编译器正常编译通过,我们在返回值类型前面加上了@UnsafeVariance
注解,这样编译器就会允许泛型T出现在out位置上了。注意不要滥用这个注解,因为运行时因此出现了类型转换异常,Kotlin对此是不负责的
那么,这个时候会产生什么样的安全隐患呢?
1 | fun main() { |
上述代码就是一个典型的违反逆变规则而造成类型转换异常的例子。在Transformer<Person>
的匿名类实现中,我们使用transform()
方法中传入的name和age参数构建了一个Teacher对象并返回。由于transform()
方法的返回值是Person对象,而Teacher是Person的子类,因此这样写是合法的
但是在handleTransforme()
方法中,我们调用了Transformer<Student>
的transform()
方法,并传入了name和age参数,期望得到的是一个Student对象返回,然而实际上transform()
方法返回的是一个Teacher对象,因此这里必然会造成类型转换异常
由于这段代码可以编译通过,我们运行一下:
可以看到程序出现异常,提示我们Teacher类型是无法转换成Student类型的
也就是说,Kotlin在提供协变和逆变功能时,就已经把各种潜在的类型转换安全隐患全部考虑进去了,只要我们严格按照其语法规则,让泛型在协变时只出现在out位置上,逆变时只出现在in上,就不会存在类型转换异常的车情况。虽然@UnsafeVariance
注解可以打破这一语法规则,但同时也会带来额外的风险,所以我们在使用@UnsafeVariance
注解时,必须清楚自己在干什么