资讯专栏INFORMATION COLUMN

对比Java泛型中的extends/super和Kotlin的out/in

LittleLiByte / 3227人阅读

摘要:使用强转的话,只能强转成和它的基类,如果强转成的子类的话,有可能会报运行时异常。拥有类型,它是的子类型因此,我们可以将赋给类型为的变量在声明处设置后,就可以和或它的子类进行比较了。

欢迎关注我的博客:songjhh"s blog

原文连接:对比Java泛型中的extends/super和Kotlin的out/in

在 Java 泛型中,有一个叫做通配符上下界 bounded wildcard 的概念。

:指的是上界通配符 (Upper Bounded Wildcards)

:指的是下界通配符 (Lower Bounded Wildcards)

相对应在 Kotlin 泛型中,有 outin 两个关键字

下面我将会以工位分配的例子解释它可以用来解决什么问题,并且对比 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 {
    private T employee;
    public WorkStation(T employee) {
        this.employee = employee;
    }
}

按照逻辑,一个经理的工位,当然也是一个员工的工位,但事实真的如此吗?

// 创建一个经理工位
WorkStation managerWorkStation = new WorkStation<>(new Manager("John"));
// 将经理工位赋给员工工位
WorkStation employWorkStation = managerWorkStation; // error

但这里会报 incompatible types: WorkStation cannot be converted to WorkStation,意思是两个类型不能相互转化。虽然 Manager 继承于 Employee ,但是两个类型的工位并没有继承关系,所以不能直接将经理工位的引用传给员工工位。

造成这个现象的原因,是因为Java 的参数类型是不型变的 invariant而通配符上下界正是为了绕过这个问题。

ps: 型变在计算机编程中,特别是面向对象编程,是重要的基石,可以在测试阶段帮助程序员发现很多的错误,这里不展开讨论。
有界限的通配符(Bounded Wildcards)

为了帮助理解和记忆,在讲通配符上下界之前,这里先讲一讲PECS原则

PECS stands for producer-extends, consumer-super 

From: Effective Java Third Edition - Item 31

这里引用的是 Effective Java Third Edition 关于如何利用 bounded wildcards 来提升 API 灵活性章节一个助记词。

简单来说,生产者适合用 ,而消费者适合用 ,这里生产者指的是能用来读取的对象,消费者指的是用来写入的对象,下面将会详细解释这两个概念。

上界通配符(extends)

还是接着上面的例子,员工的工位为了获得经理工位的引用,这里使用上界通配符

// 创建一个经理工位
WorkStation managerWorkStation = new WorkStation<>(new Manager("John"));
// 将经理工位的引用赋给一个继承于员工对象的工位
WorkStation exWorkStation = managerWorkStation;

可以看到使用了上界通配符,我们将经理工位和员工工位关联起来了,使得 Java 泛型的灵活性大大增加。

但是上面介绍了 PECS原则 , 它指出上界通配符只适合用于生产者中,下面我带大家来看看这句话如何理解:

WorkStation managerWorkStation = new WorkStation<>(new Manager("John"));
WorkStation 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

下界通配符

上界通配符恰恰相反,下界通配符 适合存储对象的场景。

WorkStation 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 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 = WorkStation(Manager("John"))
val station: WorkStation = managerWorkStation // error, type mismatch

由此看到在 Kotlin 里对泛型也是有限制的。相对于 Java 提供的 ,Kotlin 相对应提供了 outin 关键字。

在 Kotlin 中 out 相当于 in 相当于 ,这里看看用法。

out 关键字:

val managerWorkStation: WorkStation = WorkStation(Manager("John"))
val outStation: WorkStation = 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 = 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 提供的 outin 两个型变关键字还可以用于泛型声明的时候。

public interface Collection : Iterable {
    ...
}

// 错误,这里只能用val,不能用var
class Source(var t: T) {
    ...
}

在声明处设置 out 后,使得了在 Kotlin 中,Collection 安全的作为 Collection 的父类使用,但 E 被标记为 out 后,E 只能被输出而不能写入。

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
}

Comparable 在声明处设置 in 后,x 就可以和 Number 或它的子类进行比较了。

总结

以上就是 Java 和 Kotlin 关于泛型型变的内容,其中 Kotlin 对比 Java,多加了声明处型变的方式。

Java Java示例代码 Kotlin示例代码
使用处型变 void example(List list) fun example(list: List)
使用处逆变 void example(List) fun example(list: List)
声明处型变 - interface Collection : Iterable
声明处逆变 - interface Comparable

为了帮助记忆,上文引用了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...

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/73735.html

相关文章

  • Java知识点总结(Java泛型

    摘要:知识点总结泛型知识点总结泛型泛型泛型就是参数化类型适用于多种数据类型执行相同的代码泛型中的类型在使用时指定泛型归根到底就是模版优点使用泛型时,在实际使用之前类型就已经确定了,不需要强制类型转换。 Java知识点总结(Java泛型) @(Java知识点总结)[Java, Java泛型] [toc] 泛型 泛型就是参数化类型 适用于多种数据类型执行相同的代码 泛型中的类型在使用时指定 泛...

    linkin 评论0 收藏0
  • 小马哥Java项目实战训练营 极客大学

    摘要:百度网盘提取码一面试题熟练掌握是很关键的,大公司不仅仅要求你会使用几个,更多的是要你熟悉源码实现原理,甚至要你知道有哪些不足,怎么改进,还有一些有关的一些算法,设计模式等等。 ​​百度网盘​​提取码:u6C4 一、java面试题熟练掌握java是很关键的,大公司不仅仅要求你会使用几个api,更多的是要你熟悉源码实现原理,甚...

    不知名网友 评论0 收藏0
  • 第12章 元编程与注解、反射 《Kotlin 项目实战开发》

    摘要:第章元编程与注解反射反射是在运行时获取类的函数方法属性父类接口注解元数据泛型信息等类的内部信息的机制。本章介绍中的注解与反射编程的相关内容。元编程本质上是一种对源代码本身进行高层次抽象的编码技术。反射是促进元编程的一种很有价值的语言特性。 第12章 元编程与注解、反射 反射(Reflection)是在运行时获取类的函数(方法)、属性、父类、接口、注解元数据、泛型信息等类的内部信息的机...

    joyqi 评论0 收藏0
  • Java泛型

    摘要:虚拟机中并没有泛型类型对象,所有的对象都是普通类。其原因就是泛型的擦除。中数组是协变的,泛型是不可变的。在不指定泛型的情况下,泛型变量的类型为该方法中的几种类型的同一个父类的最小级,直到。 引入泛型的主要目标有以下几点: 类型安全 泛型的主要目标是提高 Java 程序的类型安全 编译时期就可以检查出因 Java 类型不正确导致的 ClassCastException 异常 符合越早出...

    woshicixide 评论0 收藏0
  • Java泛型通配符

    摘要:好了,有了这样的背景知识,我们可以来看一下上界通配了,在中,可以使用来界定一个上界,的意思是所有属于的子类,是上界,不能突破天界啊,我们具体化一下,的意思就是,所有的子类都可以匹配这个通配符。 1、上界通配符 首先,需要知道的是,Java语言中的数组是支付协变的,什么意思呢?看下面的代码: static class A extends Base{ void f(...

    sunny5541 评论0 收藏0

发表评论

0条评论

LittleLiByte

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<