java中的协变与逆变

逆变与协变用来描述类型转换(type transformation)后的继承关系,其定义:如果A、B表示类型,f(⋅)表示类型转换,≤表示继承关系(比如,A≤B表示A是由B派生出来的子类)

  • f(⋅)是逆变(contravariant)的,当A ≤ B时有f(B) ≤ f(A)成立;
  • f(⋅)是协变(covariant)的,当A ≤ B时有f(A) ≤ f(B)成立;
  • f(⋅)是不变(invariant)的,当A ≤ B时上述两个式子均不成立,即f(A)与f(B)相互之间没有继承关系。

1.2 里式替换原则

里氏替换原则的内容可以描述为: “派生类(子类)对象可以在程序中代替其基类(超类)对象。”

1.3 解释

通俗来讲,协变与逆变是描述,类型转换前继承关系和类型转化后继承关系的变化的。
此处的类型转化在java中最常见的就是泛型类。
继承关系最明显的作用就是里式替换,当一个类A可以在程序中代替另一个类B,那么A就是B的子类。

2.java数组

2.1 数组协变

java数组是协变的。即,如果有类型A ≤ B,则数组类型A[]与数组类型B[]之间也有,A[] ≤ B[]。

示例如下:

public class Tmp {
    static class Fruit {
        public void name() {
            System.out.println("fruit");
        }
    }

    static class Apple extends Fruit {
        public void name() {
            System.out.println("apple");
        }
    }

    static class GreenApple extends Apple {
        public void name() {
            System.out.println("green apple");
        }
    }

    @Test
    public void test() {
        Fruit[] fruits = new Apple[10]; // ①
        fruits[0] = new Apple();
        fruits[1] = new Apple();
        fruits[2] = new Fruit(); // ② throw java.lang.ArrayStoreException
    }
}
复制代码

上面的例子中Apple继承自Fruit,所以数组类型 Apple[]
也是 Fruit[]
的子类,所以①处是合法的,编译器不会报错,也可以正常运行。
但是 new Apple[10]
中只能放入Apple类的元素,所以②处执行时会异常,但是可以正常编译。

2.2 数组协变的原因

如果数组不协变,入参为 Fruit[]
类型的地方不能传入 Apple[]
,数组使用的时候会丧失多态的灵活性。
但是多态带来了新的问题,引用类型( Fruit[]
)和实际类型( Apple[]
)不相同,导致Apple[]可能会被放入Fruit元素。
如果 Apple[]
数组不管不顾,将Fruit对象接受,那可能导致从 Apple[]
数组中读取一个非Apple对象的Fruit对象,这是不符合数组定义的(数组(英语:Array),是由相同类型的元素(element)的集合所组成的数据结构)。
所幸的是,数组在接受对象时会做类型检查。所以不会出现上述情况。

综上,数组协变带来了灵活性,又因为类型检查,数组依然是安全的,所以数组设计成了协变的。

3.java泛型

3.1 泛型不变

一般情况下,如果类A ≤ B,那么泛型类 T<A>
和泛型类 T<B>
没什么明确的继承关系。

一句话总结,正常情况下泛型是不变的。

如果泛型始终是不变的,那么所有的泛型类之间没有任何继承关系,那泛型类就完全没有多态性,泛型存在的意义也会大幅削弱。
所以java提供了泛型约束符来实现逆变与协变。

3.2 泛型逆变

泛型逆变,即,如果类A ≤ B,那么 T<B>
T<A>

java泛型使用super关键字实现逆变。

public class Tmp {
    static class Fruit {
        public void name() {
            System.out.println("fruit");
        }
    }

    static class Apple extends Fruit {
        public void name() {
            System.out.println("apple");
        }
    }

    static class GreenApple extends Apple {
        public void name() {
            System.out.println("green apple");
        }
    }

    @Test
    public void test1() {
        List<Fruit> fruits = Arrays.asList(new Fruit(), new Fruit(), new Apple());
        eat(fruits); // ① 正常调用

        List<GreenApple> greenApples = Arrays.asList(new GreenApple());
        eat(greenApples); // ② 语法错误
    }

    private void eat(List<? super Apple> list) {
        System.out.println("eat");
    }
}
复制代码

上例中①处正常调用,说明 List<Fruit>
List<? super Apple>
的子类,所以使用super关键字可以实现 逆变

3.3 泛型协变

泛型协变,即,如果类A ≤ B,那么 T<A>
T<B>

java泛型使用extends关键字实现逆变。

public class Tmp {
    static class Fruit {
        public void name() {
            System.out.println("fruit");
        }
    }

    static class Apple extends Fruit {
        public void name() {
            System.out.println("apple");
        }
    }

    static class GreenApple extends Apple {
        public void name() {
            System.out.println("green apple");
        }
    }

    @Test
    public void test1() {
        List<Fruit> fruits = Arrays.asList(new Fruit(), new Fruit(), new Apple());
        eat(fruits); // ① 语法错误

        List<GreenApple> greenApples = Arrays.asList(new GreenApple());
        eat(greenApples); // ② 正常执行
    }

    private void eat(List<? extends Apple> list) {
        System.out.println("eat");
    }

}
复制代码

上例中②处正常调用,说明 List<GreenApple>
List<? extends Apple>
的子类,所以使用super关键字可以实现 协变

4.总结

本文的重点在于 协变
逆变
,列举了数组、泛型的例子来说明协变逆变的概念。
泛型化类或者数组,都会带来转化后的类(泛型类或数组)与原始类之间继承关系如何确定的问题。
协变与逆变这一组概念的提出就是为了确定转化后的类的继承关系。
很多语言实现泛型时其实都要考虑这个问题,据我所知C#和kotlin等语言采用了和java类似的方式。

水平有限,难免有错漏之处。有问题请联系我(cxkun992@gmail.com),大家一起讨论。

原文 

https://juejin.im/post/5ef3649bf265da22b8557653

本站部分文章源于互联网,本着传播知识、有益学习和研究的目的进行的转载,为网友免费提供。如有著作权人或出版方提出异议,本站将立即删除。如果您对文章转载有任何疑问请告之我们,以便我们及时纠正。

PS:推荐一个微信公众号: askHarries 或者qq群:474807195,里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多

转载请注明原文出处:Harries Blog™ » java中的协变与逆变

赞 (0)
分享到:更多 ()

评论 0

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址