协变 & 逆变
MoMo Lv5

概念

假设Orange类是Fruit类的子类,以集合类List为例:

  • 型变: 用来描述类型转换后的继承关系(即协变、逆变和不变的统称)。比如:List是List的子类型吗?答案是No,两者并没有关系,并不能相互读写数据。因此,型变是处理如List(List<? extends Orange>)和List子类型关系的一种表达方式。

  • 协变(covariance)(<? extends T>): 满足条件诸如List是List<? extends Fruit>的子类型时,称为协变。

  • 逆变(covariance)(<? super T>): 满足条件List是List<? super Orange>的子类型时,称为逆变。

  • 不变(invariance)(T): 表示List和List不存在型变关系。

型变

看一个不使用型变的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 1.定义一个String类型的List
*/
List<String> value1 = new ArrayList<String>();

/**
* 2. 这里编译器报错,因为两者没有型变关系,无法直接赋值,后续操作会导致类型不安全
*/
List<Object> value2 = value1; //error


/**
* 3.假如上面第2步编译通过了,那么此时add()这个整型数据1到value2中
* 是没问题的,因为他的类型是Object。但是读取时会碰到困难,如第4步。
*/
value2.add(1);

/**
* 4.但此时读出来的是什么类型呢,上一步add了一个整型数据1,此时如果用String类
* 型的变量接返回值,肯定不合适,因此运行时会报类型转换异常!!
*/
String result = value1.get(0); //error

上面举例说明了在不使用型变的情况下,对泛型数据的操作会面临种种困难,虽然保证了运行时参数类型的安全,却限制了接口的灵活性(编译器检查),比如:如果我们只调用value2(List)的get()方法,不调用add()方法(只读取数据不写入数据),显然此时不会有类型的安全问题,那如何限制只能调用get()却不能add()方法呢?当然只能靠编译器限制了,让你调add()方法的时候编译都通不过就可以了。通配符就是干这件事的,通知编译器,限制我们对于某些方法的调用,以保证运行时的类型安全。

协变

协变是指当一个类型参数 T 是另一个类型参数 S 的子类型时,容器类型 Container<T> 也是 Container<S> 的子类型。换句话说,当一个泛型类型的类型参数能够随着子类型的变化而变化时,我们就称这个泛型类型是协变的。

在 Kotlin 中,协变使用 out 关键字来声明。例如,out T 表示类型参数 T 是协变的。

1
2
3
interface Source<out T> {
fun next(): T
}

对于上面不型变的例子,我们可以做如下调整,就可以达到协变的目的:

1
2
3
4
5
6
7
8
9
  /**
* 2. 这里编译器不会报错
*/
List<? extends String> value2 = value1;

/**
* 3.但此处编译器报错了,编译器限制了写入数据的操作
*/
value2.add(1); //error

但上面的简单例子太过简单,缺少继承关系,不能明显说明问题,下面仍以Orange类是Fruit类的子类来举例说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 1.定义一个类型上界限定为Fruit的List,即协变
*/
List<? extends Fruit> fruits = new ArrayList<>();

/**
* 2.编译器报错,不能添加任何类型的数据
* 原因是:
* List.add(T t)函数通过上面的类型指定后,参数会变成
* <? extends Fruit>,从这个参数中,编译器无法知道需要哪个具体的Fruit子类型,
* Orange、Banana甚至Fruit都可以,因此,为了保证类型安全,编译器拒绝任何类型。
*/
fruits.add(new Orange());
fruits.add(new Fruit());
fruits.add(new Object());

/**
* 3.此处正常!! 由于我们定义是指定了上界为Fruit,因此此处的返回值肯定至少是Fruit类型,
* 而基类型可以引用子类型
*/
Fruit f = fruits.get(0);

通过上面代码的可以看出,协变限制了参数中带T的方法调用,比如上面的add(T t)方法(我们称之为消费者方法),而允许生产者方法的调用如T get(int position),以此来保证类型的安全。

逆变

逆变是指当一个类型参数 T 是另一个类型参数 S 的父类型时,容器类型 Container<T>Container<S> 的子类型。换句话说,当一个泛型类型的类型参数能够随着父类型的变化而变化时,我们就称这个泛型类型是逆变的。

在 Kotlin 中,逆变使用 in 关键字来声明。例如,in T 表示类型参数 T 是逆变的。

1
2
3
interface Consumer<in T> {
fun consume(item: T)
}

协变的反方向是逆变,在协变中我们可以安全地从泛型类中读取(从一个方法中返回),而在逆变中我们可以安全地向泛型类中写入(传递给一个方法)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* 1.定义一个Object的List,作为原始数据列表
*/
List<Object> objects = new ArrayList<>();
objects.add(new Object()); //添加数据没有问题
objects.add(new Orange()); //仍然没有问题,

/**
* 2.定义一个类型下界限定为Fruit的List,并将objects赋值给它。
* 此时编译不会报错,因为满足逆变的条件。
*/
List<? super Fruit> fruits = objects;

/**
* 3.add(T t)函数,编译器不会报错,因为fruits接受Fruit的基类类型,
* 而该类型可以引用其子类型(多态性)
*/
fruits.add(new Orange());
fruits.add(new Fruit());
fruits.add(new RedApple());

/**
* 4.此处编译器报错,因为fruits限定的是下界是Friut类型,因此,
* 编译器并不知道确切的类型是什么,没法找到一个合适的类型接受返回值
*/
Fruit f = fruits.get(0);

/**
* 5.此处不会报错,因为Object是Fruit的最顶层基类,满足下界的限定
*/
Object obj = fruits.get(0);

通过上面代码的注释可以看出,逆变限制了读取方法的调用,比如上面的T get(int position)方法(我们称之为生产者方法),而允许消费者方法的调用如add(T t),依次来保证类型的安全。

总结

  • 协变用于生产者(Producer),即从容器中获取元素。
  • 逆变用于消费者(Consumer),即向容器中添加元素。

extends限定了通配符类型的上界,所以我们可以安全地从其中读取;
而super限定了通配符类型的下界,所以我们可以安全地向其中写入。

我们把那些只能从中读取的对象称为生产者(Producer),我们可以从生产者中安全地读取;只能写入的对象称为消费者(Consumer)。
因此这里就是著名的PECS原则:Producer-Extends, Consumer-Super。

数组的协变

Java中数组是协变的:可以向子类型的数组赋予基类型的数组引用,由于数组在Java中是完全定义的,因此内建了编译期和运行时的检查,具体参见如下代码注释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class Fruit {}
class Apple extends Fruit {}
class Jonathan extends Apple {}
class Orange extends Fruit {}

/**
* 创建了一个Apple数组,并将其赋值给一个Fruit数组引用,编译器和运行时都允许
*/
Fruit[] fruits = new Apple[10];

/**
* 将子类对象放置到父类数组中,编译器和运行时都允许
*/
fruits[0] = new Apple();
fruits[1] = new Jonathan();

try {
/**
* 将Apple的父类对象放置到子类数组中,编译器允许,但运行时检查抛出异常
*/
fruits[2] = new Fruit();
} catch (Exception e) {
Log.i(TAG, "array exception!", e);
}

try {
/**
* 将Apple的兄弟对象放置到数组中,编译器允许,但运行时检查抛出异常
*/
fruits[3] = new Orange();
} catch (Exception e) {
Log.i(TAG, "array exception!", e);
}

自限定与协变

Java中一个常见的自限定写法是:

1
2
3
4
5
6
7
8
9
10
11
class Base<T extends Base<T>> {
T element;

T get() {
return element;
}

void set(T t) {
element = t;
}
}

这种语法定义了一个基类,这个基类能够使用子类作为其参数、返回类型、作用域。

协变参数类型

在非泛型代码中,参数类型不能随子类型发生变化。方法只能重载不能重写。在使用自限定类型时,方法接受子类型而不是基类型为参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* 自限定协变参数类型
* 方法接受只能接受子类型而不是基类型为参数
* @param <T>
*/
interface SetInterface<T extends SetInterface<T>> {

void set(T arg);
}

/**
* 具体的子类型
* 避免重写基类的方法
*/
interface SubSetInterface extends SetInterface<SubSetInterface> {}


public void test5(SubSetInterface s1, SubSetInterface s2, SetInterface sb) {
/**
* 编译通过
*/
s1.set(s2);

/**
* 只能接受具体的子类型,不能接受SetInterface基类型
*/
//s1.set(sb); //error
}

协变返回类型

继承自限定基类的子类,将产生确切的子类型作为其返回值.不过,这种实现java的多态性已经可以达到目的(基类引用子类):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 自限定协变返回类型
* @param <T>
*/
interface GetInterface<T extends GetInterface<T>> {

T get();
}


/**
* 具体的子类型
* 避免重写基类的方法
*/
interface SubGetInterface extends GetInterface<SubGetInterface> {}



public void test4(SubGetInterface g) {
GetInterface s1 = g.get();
SubGetInterface s2 = g.get();
}

Powered by Hexo & Theme Keep
Unique Visitor Page View