Kotlin中的范型和Java中的比较相似,可以参考我的关于介绍Java范型的文章 :
在上述博文中,讲述了为什么Java要在1.5版本中引入范型,以及一些有关Java范型的基本知识点。
如果把一个对象分为声明、使用两部分的话。泛型主要是侧重于类型的声明的代码复用,通配符则侧重于使用上的代码复用。泛型用于定义内部数据类型的参数化,通配符则用于定义使用的对象类型的参数化。
使用泛型、通配符提高了代码的复用性。同时对象的类型得到了类型安全的检查,减少了类型转换过程中的错误。
范型和数组的型变
Java中数组是协变的
下面的代码是可以正确编译运行的:
Integer[] ints = new Integer[3];
ints[0] = 0;
ints[1] = 1;
ints[2] = 2;
Number[] numbers = new Number[3];
numbers = ints;
for (Number n : numbers) {
System.out.println(n);
}
在Java中,因为 Integer 是 Number 的子类型,数组类型 Integer[] 也是 Number[] 的子类型,因此在任何需要 Number[] 值的地方都可以提供一个 Integer[] 值。
Java对List<T>泛型不是协变的
也就是说, List<Integer> 不是 List<Number> 的子类型,试图在要求 List <Number>的位置提供 List<Integer> 是一个类型错误。下面的代码,编译器是会直接报错的:
就算我们使用通配符,这样写:
仍然是报错的。
为什么Number的对象可以由Integer实例化,而ArrayList<Number>的对象却不能由ArrayList<Integer>实例化?list中的<? extends Number>声明其元素是Number或Number的派生类,为什么不能add Integer?为了解决这些问题,需要了解Java中的逆变和协变以及泛型中通配符用法。
逆变、协变和不变都是用来描述类型转换(type transformation)后的继承关系,其定义:如果A、B表示类型,f(⋅)表示类型转换,≤表示继承关系(比如,A≤B表示A是由B派生出来的子类)
- 当A≤B时有f(A)≤f(B)成立,则f(⋅)是协变(covariant)的
- 当A≤B时有f(B)≤f(A)成立,则f(⋅)是逆变(contravariant)的
- 当A≤B时上述两个式子均不成立,即f(A)与f(B)相互之间没有继承关系,f(⋅)是不变(invariant)的
协变和逆协变都是类型安全的。
Kotlin的数组不是协变的
abstract class Animal(val size: Int)
class Dog(val cuteness: Int): Animal(100)
class Cat(val terrorFactor: Int): Animal(1)
以下数组编译报错:
val dogArr: Array<Dog> = arrayOf(Dog(1), Dog(2))
val animalArr: Array<Animal> = dogArr
和Java的普通对象一样,以下代码可以编译通过:
val dog: Dog = Dog(10)
var animal: Animal = dog
而Kotlin中以如下方式定义一个范型类,然后使用时也会编译报错:
class ReadableList<T>{
}
val dogReadable: ReadableList<Dog> = ReadableList()
//提示报错,需要ReadableList<Animal>,但却传了ReadableList<Dog>
val animalReadable: ReadableList<Animal> = dogReadable
Kotlin对List<T>泛型是协变的
即以下代码可以编译通过
val dogList: List<Dog> = listOf(Dog(10), Dog(20))
playAnimal(dogList)
fun playAnimal(animalList: List<Animal>) {
...
}
如何使Java和Kotlin添加协变和逆变支持
Java中泛型是不变的,可有时需要实现逆变与协变,怎么办呢?这时就需要使用我们之前讲的通配符?
。
在Java和Kotlin中可以通过一定的方式对默认不支持协变的参数类型添加支持。 但是Java和Kotlin这两种语言处理的方式不同:
- Java : use-site variance(使用端型变)
- Kotlin : declaration-site variance(声明端型变)
可以看出Java使用的是使用端型变,而Kotlin使用的是声明端型变。这两者有什么区别呢?
个人理解就是使用端型变(use-site variance)就是在具体使用(初始化)某一个Class的对象时进行协变。
Java 通过<? extends T>
实现了泛型的协变
List<? extends Number> list = new ArrayList<>();
这里的? extends Number
表示的是Number类或其子类,我们简记为C。
这里C <= Number
,这个关系成立:List<C> <= List< Number >
。即有:
List<? extends Number> list1 = new ArrayList<Integer>();
List<? extends Number> list2 = new ArrayList<Float>();
另外一个例子,具体如下代码所示:
List<Cat> catList = new ArrayList<>();
List<? extends Animal> animalList = catList;
可以看到,在我们声明animalList时,对泛型进行了一点修改,使用? extends Animal进行修饰之后,以上代码就可以成功编译并运行了。甚至于我们可以定义一个方法来接受这种参数类型,如下所示:
List<Cat> cats = new ArrayList<>();
playAnimal(cats);
public static void playAnimal(List<? extends Animal> animal) {
...
}
编译可以顺利通过, 这样我们的代码可扩展性会更高!
⚠️注意:此时除了null之外,不能往animalList中添加任何Animal子类的对象,即以下代码会报错:
如果可以添加的话,List<? extends Number>
里面将会持有各种Number子类型的对象(Byte,Integer,Float,Double等等)。Java为了保护其类型一致,禁止向List<? extends Number>添加任意对象,不过可以添加null。
Java 通过<? super T>
实现了泛型的逆变
List<? super Number> list = new ArrayList<>();
? super Number
通配符则表示的类型下界为Number。即这里的父类型F是? super Number
, 子类型C是Number。即当F <=C , 有f(C) <= f(F) , 这就是逆变。代码示例:
List<? super Number> list3 = new ArrayList<Number>();
List<? super Number> list4 = new ArrayList<Object>();
list3.add(new Integer(3));
list4.add(new Integer(4));
也就是说,我们不能往List<? super Number >
中添加Number的任意父类对象。但是可以向List<? super Number >添加Number及其子类对象。
PECS:何时使用extends?何时使用super?
Joshua Bloch 称那些你只能从中读取的对象为生产者,并称那些你只能写入的对象为消费者。他建议:“为了灵活性最大化,在表示生产者或消费者的输入参数上使用通配符类型”,并提出了以下助记符:
PECS 代表生产者-Extens,消费者-Super(Producer-Extends, Consumer-Super)。
注意:如果你使用一个生产者对象,如 List<? extends Foo>
,在该对象上不允许调用 add()
或 set()
。但这并不意味着该对象是不可变的:例如,没有什么阻止你调用 clear()
从列表中删除所有项目,因为 clear()
根本无需任何参数。通配符(或其他类型的型变)保证的唯一的事情是类型安全。不可变性完全是另一回事。
声明处型变
假设有一个泛型接口 Source<T>
,该接口中不存在任何以 T
作为参数的方法,只是方法返回 T
类型值:
// Java
interface Source<T> {
T nextT();
}
那么,在 Source <Object>
类型的变量中存储 Source <String>
实例的引用是极为安全的——没有消费者-方法可以调用。但是 Java 并不知道这一点,并且仍然禁止这样操作:
// Java
void demo(Source<String> strs) {
Source<Object> objects = strs; // !!!在 Java 中不允许
// ……
}
为了修正这一点,我们必须声明对象的类型为 Source<? extends Object>
,这是毫无意义的,因为我们可以像以前一样在该对象上调用所有相同的方法,所以更复杂的类型并没有带来价值。但编译器并不知道。
在 Kotlin 中,有一种方法向编译器解释这种情况。这称为声明处型变:我们可以标注 Source
的类型参数 T
来确保它仅从 Source<T>
成员中返回(生产),并从不被消费。 为此,我们提供 out 修饰符:
interface Source<out T> {
fun nextT(): T
}
fun demo(strs: Source<String>) {
val objects: Source<Any> = strs // 这个没问题,因为 T 是一个 out-参数
// ……
}
一般原则是:当一个类 C
的类型参数 T
被声明为 out 时,它就只能出现在 C
的成员的输出-位置,但回报是 C<Base>
可以安全地作为 C<Derived>
的超类。
简而言之,他们说类 C
是在参数 T
上是协变的,或者说 T
是一个协变的类型参数。 你可以认为 C
是 T
的生产者,而不是 T
的消费者。
out修饰符称为型变注解,并且由于它在类型参数声明处提供,所以我们讲声明处型变。 这与 Java 的使用处型变相反,其类型用途通配符使得类型协变。
另外除了 out,Kotlin 又补充了一个型变注释:in。它使得一个类型参数逆变:只可以被消费而不可以被生产。逆变类型的一个很好的例子是 Comparable
:
interface Comparable<in T> {
operator fun compareTo(other: T): Int
}
fun demo(x: Comparable<Number>) {
x.compareTo(1.0) // 1.0 拥有类型 Double,它是 Number 的子类型
// 因此,我们可以将 x 赋给类型为 Comparable <Double> 的变量
val y: Comparable<Double> = x // OK!
}
我们相信 in 和 out 两词是自解释的(因为它们已经在 C# 中成功使用很长时间了), 因此上面提到的助记符不是真正需要的,并且可以将其改写为更高的目标:
存在性(The Existential) 转换:消费者 in, 生产者 out! :-)
在定义一个类的时候处理, 具体如下所示:
// 使用out关键字
class ReadableList<out T>{
}
val dogReadable: ReadableList<Dog> = ReadableList()
val animalReadable: ReadableList<Animal> = dogReadable
以上代码跟之前唯一的不同点就是我们在定义ReadableList是对泛型添加了一点限制 out, 然后就可以将dogReadable顺利的赋值给animalReadable对象. 看到这我们应该就能猜到为什么之前Kotlin API中的List<泛型>是支持协变的。
⚠️注意:但是使用out关键字修饰了之后,在ReadableList类内部不可以有以T为参数类型的方法
参考:
- https://github.com/EasyKotlin/chapter6_generics
- https://huanglizhuo.gitbooks.io/kotlin-in-chinese/content/ClassesAndObjects/Generics.html
- 深入理解Java与Kotlin的泛型(Generic Type)和型变(Variance)
- Kotlin中文网:https://www.kotlincn.net/docs/reference/generics.html