转载

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
正文到此结束
Loading...