对比Java泛型中的extends/super和Kotlin的out/in
欢迎关注我的博客:songjhh's blog
在 Java 泛型中,有一个叫做通配符上下界 bounded wildcard
的概念。
<? extends T>
:指的是上界通配符 (Upper Bounded Wildcards)<? super T>
:指的是下界通配符 (Lower Bounded Wildcards)
相对应在 Kotlin 泛型中,有 out
和 in
两个关键字
下面我将会以工位分配的例子解释它可以用来解决什么问题,并且对比 Java 来说,Kotlin 作了什么改进。
解决的问题
这里有4个实体,分别是 Employee
(员工基类),Manager
(经理), DevManager
(开发经理),WorkStation
工位。
它们的关系如下:
@Data public class Employee { private String name; public Employee(String name) { this.name = name; } } @Data public class Manager extends Employee { private Integer level; public Manager(String name) { super(name); } } @Data public class DevManager extends Manager { private String language; public DevManager(String name) { super(name); } }
其中一个工位可以坐一个员工, 这里用泛型抽象出员工来:
@Data public class WorkStation<T> { private T employee; public WorkStation(T employee) { this.employee = employee; } }
按照逻辑,一个经理的工位,当然也是一个员工的工位,但事实真的如此吗?
// 创建一个经理工位 WorkStation<Manager> managerWorkStation = new WorkStation<>(new Manager("John")); // 将经理工位赋给员工工位 WorkStation<Employ> employWorkStation = managerWorkStation; // error
但这里会报 incompatible types: WorkStation<Manager> cannot be converted to WorkStation<Employee>
,意思是两个类型不能相互转化。虽然 Manager
继承于 Employee
,但是两个类型的工位并没有继承关系,所以不能直接将经理工位的引用传给员工工位。
造成这个现象的原因,是因为Java 的参数类型是不型变的 invariant
,而通配符上下界正是为了绕过这个问题。
ps: 型变在计算机编程中,特别是面向对象编程,是重要的基石,可以在测试阶段帮助程序员发现很多的错误,这里不展开讨论。
有界限的通配符(Bounded Wildcards)
为了帮助理解和记忆,在讲通配符上下界之前,这里先讲一讲PECS原则。
PECS stands for producer-extends, consumer-superFrom: Effective Java Third Edition - Item 31
这里引用的是 Effective Java Third Edition 关于如何利用 bounded wildcards
来提升 API 灵活性章节一个助记词。
简单来说,生产者适合用 <? extends T>
,而消费者适合用 <? super T>
,这里生产者指的是能用来读取的对象,消费者指的是用来写入的对象,下面将会详细解释这两个概念。
上界通配符(extends)
还是接着上面的例子,员工的工位为了获得经理工位的引用,这里使用上界通配符 <? extends T>
// 创建一个经理工位 WorkStation<Manager> managerWorkStation = new WorkStation<>(new Manager("John")); // 将经理工位的引用赋给一个继承于员工对象的工位 WorkStation<? extends Employee> exWorkStation = managerWorkStation;
可以看到使用了上界通配符,我们将经理工位和员工工位关联起来了,使得 Java 泛型的灵活性大大增加。
但是上面介绍了 PECS原则
, 它指出上界通配符只适合用于生产者中,下面我带大家来看看这句话如何理解:
WorkStation<Manager> managerWorkStation = new WorkStation<>(new Manager("John")); WorkStation<? extends Employee> exWorkStation = managerWorkStation; // 只可以获取它和它的基类 Object a = exWorkStation.getEmployee(); Employee b = exWorkStation.getEmployee(); DevManager d = exWorkStation.getEmployee(); // error // 不可以存储 exWorkStation.setEmployee(new Employee("Sam")); // error, incompatible types: Manager cannot be coverted to capture#1 of ? extends Employee exWorkStation.setEmployee(new DevManager("James")); // error, incompatible types: DevManager cannot be coverted to capture#1 of ? extends Employee
上面的例子可以看到,使用了上界通配符只能用 get()
方法取出工位占位的类型和其基类,但是不能再用 set()
方法存对象到工位中,所以说上界通配符只适合用于生产者中。
原因也很好理解,因为编译器只知道工位坐的人是 Employee
对象或它的派生类,但不知道具体是哪个对象(编译器用 capture#1
标记占位,指这里捕获 Employee
和它的子类),所以不能够判断存入的对象是不是这个工位能够匹配的:
- 坐在
exWorkStation
的人一定是一个员工,所以可以取出Employee
exWorkStation
可能是Manager
的工位,所以这里存取TestManager
是没问题的。但问题在于它也可能是DevManager
的工位,那么TestManager
就不能坐在这个工位里了,编辑器无法判断,所以上界通配符不能用set()
方法
简而言之,上界通配符 Upper Bounded Wildcards
使得参数类型是协变的covariant
。
下界通配符
和上界通配符恰恰相反,下界通配符 <? super T>
适合存储对象的场景。
WorkStation<? super Manager> supWorkStation = new WorkStation<>(new Manager("James")); // 可以存储它和它的子类 supWorkStation.setEmployee(new DevManager("Sam")); supWorkStation.setEmployee(new Manager("Sam")); supWorkStation.setEmployee(new Employee("Sam")); // error // 只可以获取所有类的基类 - Object Object o = supWorkStation.getEmployee(); Employee e = supWorkStation.getEmployee(); // error Manager e = supWorkStation.getEmployee(); // error DevManager e = supWorkStation.getEmployee(); // error // 只能安全强转成它和它的基类 Employee employee = (Employee) o; Manager manager = (Manager) o; WorkStation<? super Manager> w = new WorkStation<>(new Manager("Sam")); // ClassCastException: Manager cannot be cast to DevManager DevManager devManager = (DevManager) w.getEmployee();
上面的例子可以看到,使用下界通配符可以用 set()
方法储存 Manager
和其子类,但只能用 get()
方法获得所有类的基类 Object
对象。使用强转的话,只能强转成 Manager
和它的基类,如果强转成 Manager
的子类的话,有可能会报 ClassCastException
运行时异常。
因为存入方便,取出数据比较麻烦,所以说下界通配符适合使用在消费者中。
究其原因,可以简单理解为,下界通配符标记了该工位至少是 Manager
的工位,所以这里无论是坐 DevManager
还是 TestManager
都没有问题。
这个就叫做逆变性(contravariance
)。
在Kotlin的世界里是怎么样的?
是 Java 世界是用通配符上下界来觉得泛型不型变的,那在 Kotlin 是怎么样的呢?
val managerWorkStation: WorkStation<Manager> = WorkStation(Manager("John")) val station: WorkStation<Employee> = managerWorkStation // error, type mismatch
由此看到在 Kotlin 里对泛型也是有限制的。相对于 Java 提供的 <? extends T>
和 <? super T>
,Kotlin 相对应提供了 out
和 in
关键字。
在 Kotlin 中 out
相当于 <? extends T>
,in
相当于 <? super T>
,这里看看用法。
out
关键字:
val managerWorkStation: WorkStation<Manager> = WorkStation(Manager("John")) val outStation: WorkStation<out Employee> = managerWorkStation // 只可以获取它和它的基类 val a: Any = outStation.employee val b: Employee = outStation.employee val c: Employee = managerWorkStation.employee val d: DevManager = managerWorkStation.employee // error, type mismatch // 不可以存储 outStation.employee = DevManager("Sam") // Setter for 'employee' is removed by type projection
in
关键字:
val inStation: WorkStation<in Manager> = WorkStation() // 可以存储它和它的子类 inStation.employee = Manager("James") inStation.employee = DevManager("James") inStation.employee = Employee("James") // error, type mismatch // 只可以获得Any val any: Any? = inStation.employee // 只能安全强转成它和它的基类 val employee: Employee = any as Employee val manager:Manager = any as Manager
由以上两个例子可以看到,Kotlin 和 Java 非常相似,只是相关的关键字有所不同而已。但毕竟 Kotlin 是号称要解决 Java 的,那么会不会哪里有所不同呢?
Kotlin 和 Java 的异同
使用处型变
在 Java 中,上下界通配符只能用在参数、属性、变量或者返回值中,不能在泛型声明处使用,所以才叫做使用处型变。
以上的 Kotlin 例子也用的是使用处型变,被称为类型投影。
所以 Java 和 Kotlin 都提供使用处型变。
声明处型变
但不同的是,Kotlin 还提供 Java 所不具备的声明处型变。
顾名思义,Kotlin 提供的 out
和 in
两个型变关键字还可以用于泛型声明的时候。
public interface Collection<out E> : Iterable<E> { ... } // 错误,这里只能用val,不能用var class Source<out T>(var t: T) { ... }
在声明处设置 out
后,使得了在 Kotlin 中,Collection<Number>
安全的作为 Collection<Int>
的父类使用,但 E
被标记为 out
后,E
只能被输出而不能写入。
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 }
Comparable
在声明处设置 in
后,x
就可以和 Number
或它的子类进行比较了。
总结
以上就是 Java 和 Kotlin 关于泛型型变的内容,其中 Kotlin 对比 Java,多加了声明处型变的方式。
Java | Java示例代码 | Kotlin示例代码 |
---|---|---|
使用处型变 | void example(List<? extends Number> list) | fun example(list: List<out Number>) |
使用处逆变 | void example(List<? super Integer>) | fun example(list: List<in Int>) |
声明处型变 | - | interface Collection<out E> : Iterable<E> |
声明处逆变 | - | interface Comparable<in T> |
为了帮助记忆,上文引用了PECS原则:producer-extends, consumer-super。
最后这里再引用Effective Java - 31 | Use bounded wildcards to increase API flexibilty里面对通配符的几个意见:
If an input parameter is both a producer and a consumer, then wildcard types will do you no good.
如果输入参数同时是生产者和消费者, 那么通配符对你来说不是一个好的选择。
Do not use bounded wildcard types as return types, if the user of a class has to think about wildcard types, there is probably something wrong with its API.
不要用界限通配符作为你的返回类型,如果类的用户必须考虑通配符类型,类的 API 或许就会出错。
If a type parameter appears only once in a method declaration, replace it with a wildcard.
如果类型参数只在方法声明中出现一次,就可以用通配符取代它。
谢谢阅读
版权声明:欢迎转载 (http://songjhh.top/2019/03/13...