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