因为Kotlin本质上还是Java, 所以Kotlin的泛型和Java泛型一样都是在编译期擦除类型的。那么Kotlin的泛型做了什么优化呢? 就是针对“协变”,“逆变”,“不变”,这些型变使用做了些许简化。
先复习下Java的型变。
Java泛型的通配符
类型系统有三种基本形态,“协变”,“逆变”,“不变”。如何理解它们要从Java的类型通配符说起。
Java泛型的通配符有两种形式:
- ?extends T: 子类型上界限定符,指定类型参数的上限。该类型必须是T或者T的子类型。
- ?super T:超类型下界限定符, 指定类型参数的下限。该类型必须是T或者T的父类型。
- 以上两者就是类型通配符。
public class Animal {
public void act(List<? extends Animal> list){
for(Animal animal in list){
animal.eat()
}
}
}
public class Cat extends Animal{}
public class Dog extends Animal{}
基于上面的代码,有两种情况:
- 虽然Cat和Animal,Dog与Animal都是继承关系,但是 List<Cat>和List<Animal>, List<Dog>和List<Animal>却都不是继承关系,不存在父子关系的类型。所以下面的代码都是编译不过的:
List<Animal> animals = new ArraryList<Animal>(); List<Dog> dogs = new ArraryList<Dog>(); List<Cat> cats = new ArraryList<Cat>(); animals = dogs; //没有继承关系,编译不过 animals = cats; //没有继承关系,编译不过
- 对于任何的List<T>, 这里的T只要是Animal的子类型,那么List<? extends Animal>就是List<T>的父类型。所以下面的代码是可以编译通过的:
List<?extends Animal> animals = new ArraryList<>(); List<Dog> dogs = new ArraryList<Dog>(); List<Cat> cats = new ArraryList<Cat>(); animals = dogs; //有继承关系,编译通过 animals = cats; //有继承关系,编译通过
-
我们不能在List<?extends Animal>中添加元素。因为对于set/add方法类型, 编译器无法知道具体类型,所以会拒绝这个调用。
List<?extends Animal> animals = new ArraryList<>(); List<?extends Animal> animals1 = new ArraryList<>(); List<Dog> dogs = new ArraryList<Dog>(); animals.add(new Dog()); //编译不过 animals.add(new Cat());//编译不过 animals.addAll(animals1 )//编译不过 animals = dogs; //编译通过, 下面的方法依旧编译不过 animals.add(new Dog()); //编译不过 animals.add(new Cat());//编译不过 animals.addAll(animals1 )//编译不过
-
如果是get方法类型, 那是允许的。因为编译器知道List<?extends Animal>可以转换成Animal类型。
List<?extends Animal> animals = new ArraryList<>(); List<Dog> dogs = new ArraryList<Dog>(); dogs.add(new Dog()); dogs.add(new Dog()); animals = dogs; Animal animal = new Animal(); animal.act(animals); //在animal的act方法中,可以读取List<?extends Animal> animals public void act(List<? extends Animal> list){ for(Animal animal in list){ animal.eat() } }
-
无界通配符, 就是单独一个?。比如List<?>,?可以代表任意类型, 也就是未知类型。同样,我们只能调用get类型的方法,不能调用add/set类型的方法。 因为add/set类型,编译器始终无法确定一种类型。
泛型和数组的型变
在Java中数组是协变的,所以下面的代码可以编译通过:
Animal[] animals = new Animal[3];
animals [0] = new Animal();
animals [1] = new Animal();
animals [2] = new Animal();
Dog[] dogs = new Dog[3];
dogs [0] = new Dog();
dogs [1] = new Dog();
dogs [2] = new Dog();
for(Dog dog in dogs){
dog .eat()
}
所以数组Dogs[]就是Animal[]的子类型。数组中可以这样做,你说编译器可以确定对象,可是List中怎么又不能确定对象了呢?也可以确定对象啊。以上所有代码中说到的编译通过或不通过,都需要了解Java中的协变,逆变以及泛型中通配符用法。
- 三种型变定义
- Animal类型(简记为 F, Father就是父类, Dog类型(简记为C,Child就是子类),它们继承关系。我们这种关系表示为: F <| C
- List<类型>, 分别记为f(F), f(C)。 我们就可以描述协变,逆变,不变了。
- 当F<|C成立时,如果有f(F)<| f(C)成立, 那么List f 叫做协变。
- 当F<|C成立时,如果有f(C)<| f(F)成立, 那么List f 叫做逆变。
- 如果上面两种关系都不成立,就是叫做不变。
- 协变和逆变都是类型安全的。
- Java中泛型是不变的
<? entends T >实现了泛型的协变
前面已经讲过,下面的代码都是可以的,所以<? entends T >实现了泛型的协变是实现了协变的。但是不能添加元素(除了null),那是因为Java觉得这样List里面会有各种各样的对象,太过于复杂,为了保护类型一致性,就设置了这个限制条件。
List<?extends Animal> animals0 = new ArraryList<Cat>();
List<?extends Animal> animals1 = new ArraryList<Dog>();
<? super T >实现了泛型的逆变
List<? super Animal> list = new ArrayList<>(), 其中? super Animal通配符表示下界为Animal, 即这里的父类型F是? super Animal, 子类型C是Animal。即当F<|C成立,有f(C)<|f(F), 这就是逆变。
实例代码如下:
List<? super Animal> list1 = new ArrayList<Animal>();
List<? super Animal> list2 = new ArrayList<Object>();
list1.add(new Dog);
list2.add(new Dog);
Object object = list1.get(0); //默认是Object
Dog dog = list1.get(0); //需要强制类型转换
也就是说, 我们不能往List<? super Animal>中添加Animal的任意父类对象。却可以向List<? super Animal>添加Animal的子类对象。好搞脑子的地方。
PECS
那么什么时候用extends, 什么时候有用super呢?根据我们前面分析的结果, 简单的说,extends适合get类型,也就消费使用型。super适合set/add类型,也就是生产生成型。我看一下典型事例:这是java.collections的copy方法
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
int srcSize = src.size();
if (srcSize > dest.size())
throw new IndexOutOfBoundsException("Source does not fit in dest");
if (srcSize < COPY_THRESHOLD ||
(src instanceof RandomAccess && dest instanceof RandomAccess)) {
for (int i=0; i<srcSize; i++)
dest.set(i, src.get(i));
} else {
ListIterator<? super T> di=dest.listIterator();
ListIterator<? extends T> si=src.listIterator();
for (int i=0; i<srcSize; i++) {
di.next();
di.set(si.next());
}
}
}
在public static <T> void copy(List<? super T> dest, List<? extends T> src)中,dest可以插入,而src被限制了只能读取。这样src就不会被修改了。
Kotlin中的泛型
前面讲了很多java的泛型, 现在终于可以将Kotlin的泛型了。
- out T和 in T
Kotlin抛弃了Java通配符 ?extends T和?super T, 取而代之的是out T和 in T。也就是说Kotlin采用了PECS。生产者和消费者。
Kotlin把保证读取数据安全的对象叫做生产者, 用out T标记。而把保证数据写入安全的对象叫做消费者, 用in T标记。
- 声明称型变, 在类或接口上定义安全泛型类型,以实现协变。
interface Source<out T>{
fun <T> next():T
}
所以下面的代码是合法的:
fun demo(str:Source<String>){
val obj:Source<Any> = str //这是合法的。
}
因为Collection接口和Map接口都实现了Iterable接口, 而Iterable接口被声明为生产者接口, 所以所有的Collection和Map对象都可以实现安全的类型协变。
val c:List<Number> = listOf(1,2,3,4)
由于Kotlin支持类型推断,listOf产生的就是List<Int>。因为List接口实现了安全的类型协变,所以可以安全地把List<Int>类型赋值给List<Number>类型变量。
- 类型投影
就是对参数做限定,避免不安全操作。 就是前面讲的对out/in的使用。
Kotlin也有对应的星投影语法*, 类似于Java的?, 不过*投影是安全的。
如果类型被声明为 interface Function<in T , out T>, 我们有以下星投影:
Function<*, String>表示Function<in Nothing, String>
Function<Int, *>表示Function<in Int, out Any?>
Function<*, *>表示Function<in Nothing, out Any?>