6.2.2 型变

型变是指类型转换后的继承关系。

Kotlin的型变分为逆变、协变和不变。

1.协变

如果A是B的子类型,并且Generic<A>是Generic<B>的子类型,那么Generic<T>可以称为一个协变类。

(1)Java上界通配符<? extends T>

Java的协变通过上界通配符实现。

如果Dog是Animal的子类,但List<Dog>并不是List<Animal>的子类。例如,下面的代码会在编译时报错:

     List<Animal> animals = new ArrayList<>();
     List<Dog> dogs = new ArrayList<>();
     animals = dogs; //incompatible types

而使用上界通配符之后,List<Dog>变成了List<? extends Animal>的子类型,即animals变成了可以放入任何Animal及其子类的List。因此,下面的代码编译是正确的:

     List<? extends Animal> animals = new ArrayList<>();
     List<Dog> dogs = new ArrayList<>();
     animals = dogs;

(2)Kotlin的关键词out

上述代码改成Kotlin的代码:

居然没有编译报错?其实,Kotlin的List跟Java的List并不一样。Kotlin的List源码中使用了out,out相当于Java的上界通配符。

当类的参数类型使用了out之后,该参数只能出现在方法的返回类型中。

(3)@UnsafeVariance

我们发现List的contains()、containsAll()、indexOf()和lastIndexOf()方法中,入参均出现了范型E,并且使用@UnsafeVariance修饰。

这里是由于@UnsafeVariance的修饰,才打破了out使用的限制,否则编译会报错。

2.逆变

如果A是B的子类型,并且Generic<B>是Generic<A>的子类型,那么Generic<T>可以称为一个逆变类。

(1)Java下界通配符<? super T>

Java的逆变通过下界通配符实现。下面的代码因为是协变的,无法添加新的对象。编译器只能知道类型是Animal的子类,并不能确定具体类型是什么,因此无法验证类型的安全性。

     List<? extends Animal> animals = new ArrayList<>();
     animals.add(new Dog()); //compile error

使用下界通配符之后,代码顺利编译通过:

     List<? super Animal> animals = new ArrayList<>();
     animals.add(new Dog());

其中,? super Animal表示Animal及其父类。所以animals可以接受所有Animal的子类添加至该列表中。

Java的上界通配符和下界通配符符合PECS(Producer Extends,Consumer Super)原则。如果参数化类型是一个生产者,则使用<? extends T>;如果它是一个消费者,则使用<? super T>。

其中,生产者表示频繁往外读取数据T,而不从中添加数据。消费者表示只往里插入数据T,而不读取数据。

可以用下面的公式帮助记忆:

     produce = output = out.
     consume = input = in.

(2)Kotlin的关键词in

in相当于Java下界通配符。

当类的参数类型使用了in之后,该参数只能出现在方法的入参中。

3.不变

默认情况下,Kotlin中的泛型类是不变的。这意味着它们既不是协变的,又不是逆变的。例如MutableList,泛型没有使用in、out,它可读可写。前面讲到的Kotlin数组也是不变的。