说到Kotlin中的out和in关键字,可能有些人都是what表情?最开始的我也见到这两个关键字的时候也是一脸懵逼,其实如果熟悉了Java的泛型之后,这两个关键字就秒懂了,其实无非还是PECS那一套,无非在kotlin中换了个名字而已,out对应Java的? extend,in对应kotlin的? super。本篇文章我结合kotlin看看这个两个关键字的具体用法(本文是看郭霖大神的第一行代码第三版而作,推荐大家看看这本书,就当是查缺补漏,而且还能学习很多kotlin的知识)。
out的使用
首先我们声明了三个类,如下:
open class Person(val name: String, val age: Int)
class Student(name: String, age: Int) : Person(name, age)
class Teacher(name: String, age: Int) : Person(name, age)
我们知道虽然Student是Person的子类,但是List<Student>并不是List<Person>的子类,因为这么做会导致类型转换异常,那么为什么这么做会导致类型转换异常呢?我们下面通过一个例子验证一下, 先声明一个SampleData类,封装了一个泛型data字段;
class Sample<T> {
var t: T? = null
fun set(t: T) {
this.t = t
}
fun get(): T? {
return t
}
}
fun main() {
val student = Student("tom", 11)
val sample = Sample<Student>()
sample.set(student)
handle(sample)//这里会编译报错
val stu: Student? = sample.get()
}
fun handle(sample: Sample<Person>) {
sample.set(Teacher("teacher1", 1))
}
假如这里允许将Sample<Student>赋值给Sample<Person>的话,那么上面的代码就是合法的,在handle内部就会出现再次赋值的情况,那么再次通过sample.get获取的时候期望获取一个Student的对象,但是实际上获取的是Teacher对象,就会出现了类型转换异常。
这里就好比List<String>中添加了一些String后,将List<String>赋值给List<Object>, 那我List<Object>中可以添加Integer吧,那么将来从List<Object>中获取的值是String还是Integer呢?就会出现类型转换异常,所以Java干脆就不允许这么做。
但是如果这里只是只读的,不能通过set设置值的话,就不会出现问题,那么这个时候Sample<Student>可不可以成为Sample<Person>的子类呢?
这个时候就要引出协变的定义了:定义如下:假如定义了一个MyClass<T>的泛型类,其中A是B的子类,同时MyClass<A>是MyClass<B>的子类的话,那么我们就可以说MyClass在T这个泛型上是协变的。
这个时候out就要派上用场了,out的意思就是泛型T只能书写在返回的位置,不能写在参数位置,我们重新书写上面的代码如下,就能将Sample<Student>赋值给Sample<Person>,编译不会报错,而且运行成功。
open class Person(val name: String, val age: Int)
class Student(name: String, age: Int) : Person(name, age)
class Teacher(name: String, age: Int) : Person(name, age)
fun main() {
val student = Student("tom", 11)
val sample2 = Sample2(student)
handle2(sample2)
val result = sample2.get()
println(result)
}
fun handle2(sample2: Sample2<Person>) {
sample2.get()
}
/**
* 使用了out之后,如果想将T放入in位置会报错,不允许放在in位置; 虽然可以使用@UnsafeVariance将其放入in位置,但是运行时候会报错运行时异常
*/
class Sample2<out T>(private var t: T?) {
fun get(): T? {
return t
}
}
假如我们想将T放在set的位置,是会报错的, 错误内容是Type parameter T is declared as 'out' but occurs in 'in' position in type T?, 我们可以模仿kotlin中List的处理方式,加上@UnsafeVariance关键字,就不会报错了,但是注意这里源码虽然将其放在了in位置上, 但是实际上只是将其用作比较是否contains某一个元素,并不会造成类型转换异常。
加上了@UnsafeVariance的代码如下,
public interface List<out E> : Collection<E> {
// Query Operations
override val size: Int
override fun isEmpty(): Boolean
override fun contains(element: @UnsafeVariance E): Boolean
override fun iterator(): Iterator<E>
}
fun handle2(sample2: Sample2<Person>) {
sample2.get()
sample2.set(Teacher("tea",18))
}
class Sample2<out T>(private var t: T?) {
fun get(): T? {
return t
}
//Type parameter T is declared as 'out' but occurs in 'in' position in type T?
fun set(t: @UnsafeVariance T?) {
this.t = t
}
}
此时运行代码就会报错:由此可见使用了out的话,就不允许将泛型放在in位置上,就不会造成类型转换异常的风险。如果想要放的话可以使用@UnsafeVariance注解,但是有可能造成类型转换异常。
Exception in thread "main" java.lang.ClassCastException: com.oman.lib.structure.Teacher cannot be cast to com.oman.lib.structure.Student at com.oman.lib.structure.Sample1Kt.main(Sample1.kt:11) at com.oman.lib.structure.Sample1Kt.main(Sample1.kt)
in的使用
in的使用自然就是逆变了,有的协变的底子,逆变相对来讲就好理解多了。
逆变的概念:假如定义了一个MyClass<T>的泛型类,其中A是B的子类,同时MyClass<B>是MyClass<A>的子类的话,那么我们就可以说MyClass在T这个泛型上是逆变的。我们下面通过一段代码来演示in的用法:
open class Person(val name: String, val age: Int)
class Student(name: String, age: Int) : Person(name, age)
class Teacher(name: String, age: Int) : Person(name, age)
interface Transformer<T> {
fun transform(t: T): String
}
fun handleTransformer(trans: Transformer<Student>) {
val student = Student("Tom", 19)
val result = trans.transform(student)
}
fun main() {
val trans = object : Transformer<Person> {
override fun transform(t: Person): String {
return "${t.name} ${t.age}"
}
}
handleTransformer(trans)//这行代码会报错
}
在上面的代码最后一行会提示需要输入Transformer<Student>对象,但是输入的却是Transformer<Person>对象, 这个时候in就派上用场了,在泛型声明的地方在T前面加上in即可解决问题,这意味着现在T只能出现在in位置,不能出现在out位置。
那么为什么in的时候泛型T不能出现在out位置呢?我们假设允许出现在out位置上,代码如下所示:
fun main() {
val trans = object : Transformer<Person> {
override fun transform(name: String, age: Int): Person {
return Teacher("tea", 12)
}
}
handleTransformer(trans)
}
interface Transformer<in T> {
fun transform(name: String, age: Int): @UnsafeVariance T
}
fun handleTransformer(trans: Transformer<Student>) {
val result = trans.transform("Tom", 19)
}
这里就会出现类型转换的异常: Exception in thread "main" java.lang.ClassCastException: com.oman.lib.structure.Teacher cannot be cast to com.oman.lib.structure.Student at com.oman.lib.structure.TestKt.handleTransformer(Test.kt:40) at com.oman.lib.structure.TestKt.main(Test.kt:31) at com.oman.lib.structure.TestKt.main(Test.kt)
这里因为Transformer的匿名实现类中返回的是一个Teacher对象,但是在handleTransformer方法中却想得到的是一个Student对象,这里就会类型转换异常。
总结
Kotlin和Java在提供协变逆变功能的时候就已经考虑了很多潜在的风险,只要我们按照语法规则的话,让泛型在协变的时候只出现在out位置上,在逆变的时候只出现在in位置上,就不会出现类型转换的异常了。另外@UnsafeVariance虽然可以打破这个限制,但是希望大家谨慎使用,有类型转换的风险。
————————————————
版权声明:本文为CSDN博主「Zhou Jiang」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/oman001/article/details/108556934