泛型中的型变(协变,逆变,不可变)

协变,逆变,不可变是各语言在支持泛型型变中通用的术语,所以有必要理解清楚其设计思想与作用,这样在语言过渡中才能有共性的认识,加快学习速度。之前没有彻底掌握,今天又重新梳理了一遍。越来越深的体会到学习+笔记+总结带来的记忆深度要超过'看懂会用'很多的。

实事求是的讲,截至本篇总结完,我依然无法100%确认我理解的是否完全正确。欢迎斧正~

一、什么是型变

Object a = new String("字符串")
复制代码

String作为Object的子类,可以直接将子类对象赋值给父类。这个操作即达到了型变

但是当使用泛型类型时,是无法型变的。

例:

 List<String> strs = new ArrayList<String>();
 List<Object> objs = strs;// !!!此处的编译器错误让我们避免了之后的运行时异常
//假设我们忽略这个错误继续操作,假设objs = strs 也支持型变
 objs.add(1);// 这里我们把一个整数放入一个字符串列表
 String s = strs.get(0);// !!! ClassCastException:无法将整数转换为字符串
复制代码

Java中为了保证运行时安全,所以在禁止了这样的操作

实际应用中,开发者是需要语言对泛型类型的型变的支持。所以引出了协变,逆变,不可变的实现思想。以此来支持泛型的型变

在重点开始前先做两个准备工作

1.准备几个类并明确继承关系,每向右递进一次,作为一次子类实现。

  • Fruit
    • Banana
    • Apple
      • RedApple
      • GreenApple
  1. 准备两个支持泛型的类Orchard(果园模拟产出角色:输出),Shop(店铺模拟处理角色:输入)
    public static class Orchard<T> {
        T t;
        public void set(T t) {
            this.t = t;
        }
        public T get() {
            return t;
        }
    }
    public static class Shop<T> {
        T t;
        public void set(T t) {
            this.t = t;
        }
        public T get() {
            return t;
        }
    }
复制代码

二、什么是协变(Covariance)

概念:一个类型规则或者类型构造器保持了子继承父的类型关系,即:子类型≦父类型。

作用:能够接收比原始指定的派生类型的派生程度更大(更具体的)的类型

实际开发中,需要子泛型类型能够型变为父泛型类型。

直觉上的实现代码:

Orchard<Fruit> orchardF = new Orchard<Apple>();
复制代码

这当然是无法通过编译的,需要扩大左边类型的赋值范围,需使用通配符? extends X来接受更大的类型范围

   Orchard<? extends Fruit> orchardF = new Orchard<Apple>();
复制代码

扩大了接受类型为Fruit 及其子类。依然维持读取类型为Fruit,不管协变类型是哪个Fruit子类,读取Fruit都是类型安全的。

Orchard<? extends Fruit> orchardF = new Orchard<Apple>();       
Fruit fruit = orchardF.get();
复制代码

不可以进行写入,以下代码将报错:

Orchard<? extends Fruit> orchardF = new Orchard<Apple>();
orchardF.set(new Apple());
复制代码

从直觉上似乎这样的操作并不应该存在问题。

假设将读取与写入的边界都设置为Fruit及其子类,下面的例子会有什么问题吗?

Orchard<? extends Fruit> orchardF = new Orchard<Apple>();
orchardF.set(new Banana());//报错类型不匹配
复制代码

很明显从orchardF对象的使用者来看它可以接受任何Fruit的子类,但是这实际上是给仅支持Apple类型的对象传入了一个Banana对象,这当然是不正确的。

即:在使用? extends E时,并不知道什么对象符合那个未知的E的子类型,所以无法支持写入。

java中的协变:在java中通常将通配符? extends E限制上边界来完成协变(限定输出类型的上边界)。保证了接受类型协变,读取类型安全

三、什么是逆变(Contravariance)

概念:一个类型规则或者类型构造器逆转了子继承父的类型关系,即:以父类型接受最后输出子类型

作用:能够接收比原始指定的派生类型的派生程度更小(不太具体的)的类型

在进行消费操作时如果需要使用父类型的操作,这需要下面的实现

 Shop<Apple> shopA = new Shop<Fruit>();
复制代码

这也是不被允许的,直觉上也不对呀。

需要使用? super X通配符来扩大接收类型范围,来接收实际操作对象是泛型为父类型Fruit的具体对象。

Shop<? super Apple> shopA = new Shop<Fruit>();
复制代码

这样似乎看起来有些问题,扩大了类型接受范围,也扩大输入范围,因为声明类型默认是以实际泛型类型作为上边界的,像下面这样:

Shop<Fruit> shopF = new Shop<Fruit>();
shopF.set(new Apple());
shopF.set(new Banana());
复制代码

使用了父类型,同时又被迫扩大了输入范围(期望是只输入Apple及其子类),这显然不正确。

这便是? super X通配符的作用所在,扩大赋值类型范围,限制输入类型范围,称之为限制下边界

输入示例:

Shop<? super Apple> shopA = new Shop<Fruit>();
shopA.set(new Apple());
shopA.set(new RedApple());
shopA.set(new Banana());//编译错误
复制代码

关于下边界的理解,在上面的例子中:? super Apple 将Apple指定为对象Shop<Fruit>()的输入类型的下边界。

java中的逆变:在java中通常将通配符? super E 限制下边界,允许声明的泛型为子类接收泛型为父类的对象,完成逆变。限定输入类型的下边界,从输入处隔绝了类型不安全。

四、不可变性(Invariance)

概念:不满足协变的同时不满足逆变即为不可变。

不使用通配符的情况下都是不可变的。

Shop<Fruit> shopF = new Shop<Fruit>()
复制代码

总结

概念是别人思考的结果的总结,还是要基于独立的思考与实践分析才能理解透彻。

抛开概念,实际开发中可借助Joshua Bloch 的助记符:

PECS 代表生产者-Extends、消费者-Super(Producer-Extends, Consumer-Super)

参考:

kotlin 中文文档:泛型>型变部分

本篇的由来其实就是因为在阅读文档时遇到了阻力。此篇文档重点不是介绍型变的本质,所以自己恶补了以下。为的是在理解 kotlin 泛型实现中能够正确无误。

微软的文档:泛型中的协变和逆变

本篇中对于相关概念的总结,更直观,更好理解。此篇的阐述中借助了相当一部分。

wikipedia:协变与逆变

掘金的主题 好有趣,比如本篇使用的,看着就有一种轻松的感觉。

分类:
Android
标签: