Java与Kotlin中的协变、逆变、不变

首先假设有如下几个类:

1
2
3
4
5
6
7
open class GrandFather

open class Father : GrandFather()

open class Son : Father()

open class Baby : Son()

概念

协变

协变指的是如果有一个类型T,它可以被替换为它或者它的子类型。

比如只有年龄不大于七十岁的才能买保险,那么就可以说Insurance<Father>是协变的,因为它可以被替换为Insurance<Son>Insurance<Baby>

逆变

协变指的是如果有一个类型T,它可以被替换为它或者它的父类型。

比如只有成年人才能开车,那么就可以说Driver<Son>是逆变的,因为它可以被替换为Insurance<Father>Insurance<GrandFather>

不变

不变指的是如果有一个类型T,它既不能被替换为子类型,也不能被替换为父类型。

比如只有满足特定年龄段才能上学,那么就可以说Student<Son>是不变的,它不能被替换。


协变的语言支持

赋值处协变

两种语言都默认支持。

在Java中:

1
Father father = new Son();

在Kotlin中:

1
val fathers = Son()

方法重写处协变

两种语言都默认支持,其中参数必须是不变的,返回值可以是协变的。

比如在Kotlin中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

interface MyInterface {
fun doSomething(father: Father) : Father
}

class MyInterfaceImplA: MyInterface {
// OK
override fun doSomething(father: Father): Son {
TODO()
}
}

class MyInterfaceImplB: MyInterface {
// NOT OK
override fun doSomething(father: Son): Father {
TODO()
}
}

MyInterfaceImplA将返回值的类型从Father变成了Son,这是支持的,MyInterfaceImplB将参数的类型同样从Father变成了Son,就会导致编译不过,因为参数是不支持协变的。

数组协变

Java支持,但是有缺陷,会导致运行时错误,比如这段代码:

1
2
3
4
5
6
public class Test {
public static void main(String[] args) {
Father[] fathers = new Baby[5];
fathers[0] = new Son();
}
}

运行后就会得到如下异常:

1
2
Exception in thread "main" java.lang.ArrayStoreException: Son
at Test.main(Test.java:4)

因为一开始把fathers协变成了Baby[],然后下一行又尝试把一个Baby的父类Son放入里面,就需要将父类提升成子类,这样的操作是不支持的。

为了解决这个问题,Kotlin是不支持数组协变的。比如下面这段代码,是无法通过编译的:

1
2
3
4
fun main(args: Array<String>) {
// NOT OK
val fathers: Array<Father?> = arrayOfNulls<Baby>(10)
}

泛型集合协变

Java支持,需要显式调用,用<? extends>表示。

1
2
3
4
5
6
7
8
9
public class Test {
public static void main(String[] args) {
// NOT OK
List<Father> fathers = new ArrayList<Baby>();

// OK
List<? extends Father> anotherFathers = new ArrayList<Baby>();
}
}

Kotlin支持,需要显式调用,用out表示。

1
2
3
4
5
6
7
fun main(args: Array<String>) {
// NOT OK
val fathers: MutableList<Father> = mutableListOf<Baby>()

// OK
val anotherFathers: MutableList<out Father> = mutableListOf<Baby>()
}

需要注意的是,协变后的泛型集合就变成了只读的,两种语言都不支持在协变后的集合上进行写操作,协变后的集合不能被作为参数,因此也就无法被修改。

下面这段代码是过不了编译的:

1
2
3
4
5
6
7
8
public class Test {
public static void main(String[] args) {
List<? extends Father> anotherFathers = new ArrayList<Baby>();

// NOT OK
anotherFathers.add(new Son());
}
}

这是因为集合的协变可能会导致和Java中数组协变同样的问题,也就是将一个父类放入子类的集合中去,详细分析如下:

Type Original Type Possible List Type Available Element Type
GrandFather
Father * * *
Son * *
Baby * *

集合一开始的类型是List<Father>,它可能被协变为List<Son>List<Baby>,在向集合中加入元素的时候,用的规则是赋值处协变的规则,即所有Father的子类都可以被加入,这时候只要协变后的集合类型层级比加入的元素层级高就会出错,比如集合协变成了List<Baby>,然后向其中加入Son的元素,就会导致ArrayStoreException的错误,因此两种语言都禁止了对协变后集合的写操作。


逆变的语言支持

赋值处逆变

两种语言都不支持。

在Java中:

1
2
// NOT OK
Father father = new GrandFather();

在Kotlin中:

1
2
// NOT OK
val fathers = GrandFather()

数组逆变

两种语言都不支持。

在Java中:

1
2
3
4
5
6
public class Test {
public static void main(String[] args) {
// NOT OK
Father[] fathers = new GrandFather[5];
}
}

在Kotlin中:

1
2
3
4
fun main(args: Array<String>) {
// NOT OK
val fathers: Array<Father?> = arrayOfNulls<GrandFather>(10)
}

方法重写处逆变

两种语言都不支持。无论是方法参数还是返回值都不行。

比如在Kotlin中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface MyInterface {
fun doSomething(father: Father) : Father
}

class MyInterfaceImplA: MyInterface {
// NOT OK
override fun doSomething(father: GrandFather): Father {
TODO()
}
}

class MyInterfaceImplB: MyInterface {
// NOT OK
override fun doSomething(father: Father): GrandFather {
TODO()
}
}

泛型集合逆变

Java支持,需要显式调用,用<? super>表示。

1
2
3
4
5
6
7
8
9
public class Test {
public static void main(String[] args) {
// NOT OK
List<Father> fathers = new ArrayList<GrandFather>();

// OK
List<? super Father> anotherFathers = new ArrayList<GrandFather>();
}
}

Kotlin支持,需要显式调用,用in表示。

1
2
3
4
5
6
7
fun main(args: Array<String>) {
// NOT OK
val fathers: MutableList<Father> = mutableListOf<GrandFather>()

// OK
val anotherFathers: MutableList<in Father> = mutableListOf<GrandFather>()
}

需要注意的是,逆变后的泛型集合就变成了只写的,两种语言都不支持在协变后的集合上进行读操作,逆变后的集合不能被作为返回值,因此也就不能被读取。

比如下面这段代码是过不了编译的:

1
2
3
4
5
6
7
8
public class Test {
public static void main(String[] args) {
List<? super Father> anotherFathers = new ArrayList<GrandFather>();

// NOT OK
Father object = anotherFathers.get(0);
}
}

原因和协变禁止写操作的理由类似,原始集合的类型是List<Father>,可以被逆变成List<GrandFather>,如果集合逆变成了List<GrandFather>,从中取出元素时就需要把元素提升转换为原始类型Father,而这样的操作是不支持的。

Type Original Type Possible List Type Available Element Type
GrandFather *
Father * * *
Son *
Baby *

常见用法

协变

一种常见的用法是在方法的返回值上协变,比如:

1
2
3
4
5
6
7
public GrandFather doSometing() {
return new Father();
}

public List<GrandFather> doSomethingElse() {
return Arrays.asList(new Father(), new Son());
}

逆变

协变常常用于比较器上,比如有如下两个比较器:

1
2
3
4
5
6
public class SonComparator implements Comparator<Son> {
@Override
public int compare(Son o1, Son o2) {
return 0;
}
}
1
2
3
4
5
6
public class FatherComparator implements Comparator<Father> {
@Override
public int compare(Father o1, Father o2) {
return 0;
}
}

假设有一个方法接收一个比较器来进行Son之间的比较,这种情况下用它父类的比较器来比较也是可以理解的,因为Son的所有属性在它的父类里都有,所以以下代码可以运行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Test {
public static void main(String[] args) {
SonComparator sonComparator = new SonComparator();
FatherComparator fatherComparator = new FatherComparator();


compare(sonComparator);
compare(fatherComparator);
}

private static int compare(Comparator<? super Son> comparator) {
Son sonA = new Son();
Son sonB = new Son();

return comparator.compare(sonA, sonB);
}
}

可以看到,虽然compare()方法进行的是Son之间的比较,但是传入一个类型为Comparator<Father>的比较器仍然可以运行。


参考:

Kubernetes下Jenkins CI的搭建

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×