注意: 这篇文章几年前写的,回头看内容存在很多问题,对这些概念解释的也很不清晰,不建议观看,后续可能会更新文章内容,或者重新发一篇新的。这篇留着作为曾经的记忆
Subtyping
要想了解变异先要理解Subtyping的概念。Subtyping是面向对象里”类型多态(Type Polymorphism)”的其中一种表现形式,它主要描述”is a”这样的关系。比如S是T的子类型,那么他们的关系可以表达为 S is a subtype of T。维基百科有一段对Subtyping的描述。
In programming language theory, subtyping (also subtype polymorphism or inclusion polymorphism) is a form of type polymorphism in which a subtype is a datatype that is related to another datatype (the supertype) by some notion of substitutability.
If S is a subtype of T, the subtyping relation is often written S <: T, to mean that any term of type S can be safely used in a context where a term of type T is expected
Subtyping和变异有什么联系呢?变异其实就是指Subtyping在更复杂的场景下,比如If S is a subtype of T, Generic<S> is subtype of Generic<T>这种关系是否还能成立。
变异 (Variance)
“变异”,在面向对象编程中是非常重要的一个概念。在Java8新增的Stream API和Functional中大量使用了<? extends T> 和 <? super T>这类语法,不从根本上去理解Java的变异,看到这些源码时就会有点吃力和费解。
变异,英文单词是Variance,或者称为变型。维基百科对变异的解释为
Variance refers to how subtyping between more complex types relates to subtyping between their components
通过上面对Subtyping的介绍,我们也可以得知变异和Subtyping关系是非常密切的。那么上面提到的更复杂的场景是指什么呢?举个例子,现在有Fruit、Apple、Orange三个类; 已知 Apple和Orange是Fruit的子类型。
class Fruit {
}
class Apple extends Fruit {
}
class Orange extends Fruit {
}
在Java中,我们可以这样表达它们之间的关系,这也是我们上面提到的Subtyping。
Fruit apple = new Apple();
Fruit orange = new Orange();
上面的语句是完全合法的,因为Fruit是Apple和Orange的父类型,因此我们可以描述”苹果是水果的子类型”,”橘子是水果子类型”这样的一类关系。不过,当关系变得更加复杂时,它们的关系可能会变得难以捉摸。比如装苹果的篮子是水果篮子的子类型吗?
class Basket<T> {
T t;
public void set(T t) {
this.t = t;
}
public T get() {
return this.t;
}
}
//这种关系是否正确?
Basket<Fruit> basket = new Basket<Apple>();
Basket
变异分为下面几种形式
- 不变(Invariance)
- 协变(Covariance)
- 逆变(Contravariance)
- 双变(Bi-variance)
因为java中不存在双变,本文不与讨论这种场景。下面将会分开讨论前3者具体的作用,以及他们的使用场景。
不变 (Invariance)
不变描述的是这样一种关系: 如果B是A的子类型(SubType),那么GenericType<B> 不是 GenericType<A>的子类型。从上面的介绍可以看到在Java中,泛型是不变的。
Basket<Fruit> basket = new Basket<Apple>(); //编译报错
上面代码编译会直接报错,因为在Java中泛型是不变的。
优点
保证类型安全,因为水果篮子并不一定是装着苹果,也可以装橘子。如basket.set(new Orange()),我们期望的是一个苹果,如果set一个橘子,这样无法保证类型安全。 不变的泛型从根本上杜绝了类型安全问题。
缺点
虽然保证了类型安全,不过灵活性大大下降,因为我们无法描述Basket
协变 (Covariance)
如果B是A的子类型,那么GenericType<B> 是 GenericType<A>的子类型。或者更通俗一点,以上面苹果橘子为例,协变描述的是: 装苹果的篮子是水果篮子这样一种关系。
很遗憾,Java不支持Declaration-site variance,不过Java支持Use-site variance。
Java中的协变
虽然Java的泛型不是协变,不过数组是协变的。
Fruit[] fruits = new Apple[2]; //合法
fruits[0] = new Apple(); //合法
fruits[1] = new Orange(); //编译时合法,运行时 throw ArrayStoreException
我们在泛型中使用”extends”关键字,可以让Java的泛型支持协变。
public static void covariance() {
Basket<? extends Fruit> basket = new Basket<Apple>(); //合法
List<? extends Fruit> list = new Arraylist<Orange>(); //合法
//basket.set(new Orange()); //报错
//basket.set(new Apple()); //报错
//basket.set(new Fruit()); //报错
Basket<Apple> applesOfBasket = new Basket<>();
applesOfBasket.set(new Apple());
basket = applesOfBasket; // 正确
// list同理
}
如我们所见,每次调用set时都无法通过编译,因为此时编译器并不知道确切的类型。Basket<? extends Fruit>表达的意思是: 一个Basket类型,它的Type Argument是一个extends Fruit的未知类型。这样的目的是为了保证类型安全。
Basket<? extends Fruit> basket = xx这种叫Use-site variance。JEP300有一个提案,说不定java以后会支持Declaration-site variance,有兴趣的朋友可以自己去瞄几眼。JEP 300: Augment Use-Site Variance with Declaration-Site Defaults。
如果支持Declaration-site variance,下面语句将会合法。
interface Basket<covariance T> {
}
Basket<Apple> apples = new Basket<>();
Basket<Fruit> fruits = apples;
优点
显而易见的,协变可以让我们的编码变得更加灵活。
逆变 (Contravariance)
在Subtyping一节有提到,当S是T的子类型,那么他们的关系可以表达为 S is a subtype of T,记作 S <: T 逆变就是逆转次序,T is a supertype of S,记作 T :> S。
//协变 S<:T
Basket<? extends Fruit> basket1 = new Basket<>();
//逆变 T:>S
Basket<? super Apple> basket2 = new Basket<Fruit>();
仔细看上面代码,协变和逆变在代码描述上面,次序是相反的。
类型安全
逆变同样是无法保证类型安全,不过它和协变不一样,它支持更新或添加数据。
Basket<? super Apple> basket = new Basket<Fruit>();
basket.setApple(new Apple()); //合法
Apple apple = basket.get(); //合法
Basket<Fruit> tmp = new Basket<Fruit>();
tmp.set(new Orange());
Basket<? super Apple> basket2 = tmp;
// Apple apple2 = basket2.get(); //无法通过编译
Apple apple2 = (Apple)basket2.get(); //ClassCastException
上面倒数第二行代码会报错,原因是Java无法保证你取出来的一定是Apple,这个很好理解。代码最后一行运行时会抛出ClassCastException,因为我们放进去的是Orange,因此逆变也无法保证类型安全。
PECS
PECS 全称是 Producer Extends Consumer Super,我觉得叫Supplier Extends Consumer Super更贴切一些。这是《effective java》作者提出的一种什么情况下选择extends和super的办法。
stackoverflow有个答案解释的很不错,简单来说需要获取(get)T的实例时,使用extends;当需要使用T的实例时,用super。原因上面描述过了,extends虽然支持协变,但是无法知道T到底是什么类型,因此无法使用。
This means that when a parameterized type being passed to a method will produce instances of
T(they will be retrieved from it in some way),? extends Tshould be used, since any instance of a subclass ofTis also aT.When a parameterized type being passed to a method will consume instances of
T(they will be passed to it to do something),? super Tshould be used because an instance ofTcan legally be passed to any method that accepts some supertype ofT. AComparatorcould be used on aCollection, for example.? extends Twould not work, because aComparatorcould not operate on aCollection.Ref: https://stackoverflow.com/a/2248503
结语
通过本文叙述,相信一定程度上能让你理解变异。如果熟悉面向对象编程的概念,会更加容易理解。所谓”面向对象”存在着大量的定义和概念。定义,必须严谨,必定晦涩难懂,因此对于初学者会很不友好。于是很多初学者(包括我~)刚接触面向对象编程时就会找一些”通俗易懂”的资料方便理解。但”通俗”通常意味着不严谨,存在信息丢失,因此如果想真正学好面向对象编程,还是非常有必要看看它们原本的定义和概念。
个人觉得支持Declaration-Site Variance Java泛型才算完整。不过让人吐槽的是JEP 2014年就开始提出,直到2019年了也没见出现在JSR,标准化流程的效率真是漫长啊…希望在Java能早日用上Declaration-Site Variance。
参考资料
Declaration-site and use-site variance explained
Covariance and contravariance (computer science)
JEP 300: Augment Use-Site Variance with Declaration-Site Defaults
https://stackoverflow.com/a/2248503