协变(Covariant)、逆变(Contravariant)和不变(Invariant)

1. 什么是协变、逆变和不变

协变(Covariant)逆变(Contravariant)不变(Invariant)是在计算机科学中,描述具有父子关系的多个类型通过类型构造器构造出的多个复杂类型之间是否有父子关系的用语。

协变(Covariant)、逆变(Contravariant)和不变(Invariant)属于变型(Variant)的三种结果。

简单来说,对于具有继承关系的两个类AB,假设AB的父类,那么由A构造(或变换出)的复杂类型A'和由B构造(或变换出)的复杂类型B'存在以下三种关系:

  • A'B'的父类,那么就说以上的变换是协变的;
  • B'A'的父类,那么就说以上的变换是逆变的,跟AB的继承关系相反;
  • A'B'不存在继承关系,那么就说以上的变换是不变的。

2. Java中的协变、逆变和不变

2.1 数组

数组是协变(Covariant)的,例如NumberInteger的父类,Number[]也是Integer[]的父类。

public class ArrayCovariantTest {
    public static void main(String[] args) {
        Integer[] integers = {1, 2, 3};
        Number[] numbers = integers;
    }
}

2.2 泛型类

2.2.1 普通(无类型通配符?)泛型类

对于普通(无类型通配符?)的泛型类而言,

  • 如果AB变换成了XXX<A>XXX<B>,则变换关系是不变(Invariant)的。例如, NumberInteger的父类,而List<Number>却不是List<Integer>的父类。
  • 如果AB变换成了A<XXX>B<XXX>,则变换关系是协变(Covariant)的。例如,ArrayListAbstractList的父类,ArrayList<String>也是AbstractList<String>的父类。
/**
 * 普通泛型类不变(Invariant)示例
 */
public class InvariantTest {
    public static void main(String[] args) {
        List<Number> numbers = new ArrayList<>();
        numbers.add(1);
        numbers.add(1.1);
        // 编译通过
        handle(numbers);

        List<Double> doubles = new ArrayList<>();
        doubles.add(1.1);
        doubles.add(2.2);

        // 错误示例一:赋值
        // 编译报错,Incompatible types. Required: List<java.lang.Number> Found: List<java.lang.Double>
        // 因为numbers不是doubles的父类
        numbers = doubles;

        // 错误示例二:函数调用
        // 编译报错,handle(java.util.List<java.lang.Number>) cannot be applied to(java.util.List<java.lang.Double>)
        // 因为numbers不是doubles的父类
        handle(doubles);
    }

    public static void handle(List<Number> numbers) {
        // do something
        // ...

        // 可以往numbers容器里添加任何Number或其子类的对象,例如添加一个Integer数字。
        // 注意:假如允许协变,即List<Number>是List<Integer>、List<Double>的父类。
        //      如果传入的numbers是个List<Double>容器,那么往里面存Integer数字显然就不合理。
        //      因此,Java编译器为了保证类型安全(List<Double>容器只能存放Double数字),
        //      在用户调用handle方法时,禁止传入List<Integer>或List<Double>对象。
        numbers.add(1);
    }
}
/**
 * 普通泛型类协变(Covariant)示例
 */
public class ArrayListCovariantTest {
    public static void main(String[] args) {
        ArrayList<String> strings = new ArrayList<>();
        AbstractList<String> abstractList = strings;
    }
}
2.2.2 通配符泛型类

对于NumberInteger,如果想要List<Number>List<Integer>存在继承关系,那应该怎么办呢?

Java提供了类型统配符?来帮助程序员声明一个不确定的类型,并且提供了上界通配符? extends下界通配符? super语法来限定不确定性的范围。

对于List<?>而言,?表示泛型的类型参数是不确定的,可能是IntegerDoubleString等,因此,可以将任意具体的泛型类赋值给List<?>。为了避免出现上述在List<Double>容器中添加Integer数字的情况,Java中的类型通配符PECS原则(Producer Extends Consumer Super)规定,?不能添加元素。对于List<?>的容器而言,不能添加元素,只能读取元素(只能读取为Object类型)。更准确地说,不能调用List类定义List<E>method(E e)方法,但可以调用E method()方法。IntegerNumber变换为List<Integer>List<?>,仍然保持一致的继承关系,即List<?>List<Integer>的父类,可以将这种变换理解成协变(Covariant)

/**
 * 泛型通配符协变(Covariant)示例
 */
public class WildcardsCovariantTest {
    public static void main(String[] args) {
        // 右边为Integer容器,将其引用赋值给左边的wildList
        List<?> wildList = new ArrayList<Integer>(Arrays.asList(9, 10, 11));

        // 编译报错,不允许调用method(E e)方法,
        // add的方法签名为boolean add(E e),是method(E e)形式
        // wildList指向的是Integer容器,往里面添加22.22这个Double数字显然不合理,存在类型安全问题,编译器禁止调用该方法
        wildList.add(22.22);

        // 编译报错,不允许调用method(E e)方法
        // set的方法签名为E set(int index, E element),是method(E e)形式
        // wildList指向的是Integer容器,将索引为1的元素设置为"this is a string"这个字符串显然不合理,存在类型安全问题,编译器禁止调用该方法
        Object s = wildList.set(1, "this is a string");

        // 编译通过,可以调用E method()方法,但只能读取为Object,需要自行强转为对应的类型
        // get的方法签名为E get(int index),是E method()形式
        Integer i = (Integer) wildList.get(1);
    }
}

对于List<? extends Number>而言,泛型的类型参数是有上界的,即类型参数必须是NumberNumber的子类(如IntegerDouble等),因此,可以将任意类型参数为Number及其子类的具体泛型类(如List<Number>List<Integer>List<Double>等)赋值给List<? extends Number>。同样,为了避免出现上述在List<Double>容器中添加Integer数字的情况,Java中的泛型通配符PECS原则(Producer Extends Consumer Super)规定,? extends不能添加元素。对于List<? extends Number>的容器而言,不能添加元素,只能读取元素(读取为Number类型及其父类型)。IntegerNumber变换为List<Integer>List<? extends Number>,仍然保持一致的继承关系,即List<? extends Number>List<Integer>的父类,因此变换是协变(Covariant)的。

/**
 * 泛型通配符上界协变(Covariant)示例
 */
public class UpperBoundsWildcardsCovariantTest {
    public static void main(String[] args) {
        // 右边为Integer容器,将其引用赋值给左边的wildList
        List<? extends Number> wildList = new ArrayList<Integer>(Arrays.asList(9, 10, 11));

        // 编译报错,不允许调用method(E e)方法,
        // add的方法签名为boolean add(E e),是method(E e)形式
        // wildList指向的是Integer容器,往里面添加22.22这个Double数字显然不合理,存在类型安全问题,编译器禁止调用该方法
        wildList.add(22.22);

        // 编译报错,不允许调用method(E e)方法
        // set的方法签名为E set(int index, E element),是method(E e)形式
        // wildList指向的是Integer容器,将索引为1的元素设置为"this is a string"这个字符串显然不合理,存在类型安全问题,编译器禁止调用该方法
        Object s = wildList.set(1, "this is a string");

        // 编译通过,可以调用E method()方法
        // get的方法签名为E get(int index),是E method()形式,返回的类型是通配符上界,即Number类型
        Number n = wildList.get(1);
    }
}

对于List<? super Integer>而言,泛型的类型参数是有下界的,即类型参数必须是IntegerInteger的父类(即NumberObject),因此,可以将任意类型参数为IntegerInteger的父类的具体泛型类(如List<Integer>List<Number>List<Object>等)赋值给List<? super Integer>。Java中的泛型通配符PECS原则(Producer Extends Consumer Super)规定,? super可以添加元素,也可以读取元素。对于List<? super Integer>的容器而言,可以添加元素(只能是Integer类型或其子类),也能读取元素(只能读取为Object类型)。IntegerNumber变换为List<? super Integer>List<Number>,但继承关系反转了,List<? super Integer>变成了List<Number>的父类,因此变换是逆变(Contravariant)的。

/**
 * 泛型通配符上界逆变(Contravariant)示例
 */
public class LowerBoundsWildcardsContravariantTest {
    public static void main(String[] args) {
        // 右边为Integer容器,将其引用赋值给左边的numbers
        List<Number> numbers = new ArrayList<>(Arrays.asList(1, 2.2, 3.3, 4));

        // 继承关系反转了,原本Number是Integer、Double的父类
        // 现在List<? super Integer>是List<Number>的父类
        List<? super Integer> wildList = numbers;

        // 可以添加元素,但只能添加Integer类型及其子类型(注:Integer没有子类型)
        // add的方法签名为boolean add(E e),是method(E e)形式
        // numbers原本就是个Number容器,可以存Integer,也可以存Double,因此这里的add不会有类型安全问题
        wildList.add(1);

        // 可以读取元素,但只能读取为Object类型,需要自行强转
        // get的方法签名为E get(int index),是E method()形式
        Integer i = (Integer) wildList.get(1);
        Double d = (Double) wildList.get(2);
    }
}

3. Java泛型什么时候使用协变?什么时候使用逆变?

综上所述,总结如下:

  • 当泛型类的类型参数完全不确定时,可以使用类型通配符?;当确定上界时,可以使用类型上界通配符? extends;当确定下界时,可以使用类型下界通配符? super
  • 类型通配符?和类型上界通配符? extends用于实现协变,类型下界通配符? super用于实现逆变
  • 根据PECS原则,使用?的泛型类不能调用形如method(E e)的成员方法,只能调用形如E method()的成员方法。E method()的返回值可以赋值给EE的父类变量;E method()的返回值只能赋值给Object变量,需要自行强转类型。
  • 根据PECS原则,使用? extends E的泛型类不能调用形如method(E e)的成员方法,只能调用形如E method()的成员方法。E method()的返回值可以赋值给EE的父类变量。
  • 根据PECS原则,使用? super E的泛型类可以调用形如method(E e)的成员方法,也可以调用形如E method()的成员方法。其中,method(E e)的实参可以是EE的子类对象;E method()的返回值只能赋值给Object变量,需要自行强转类型。

当泛型类有不确定的类型参数时,如果需要对泛型类的元素(泛型成员变量)进行增删改,即调用形如method(E e)的成员方法,应该使用? super E;如果需要对泛型类的元素(泛型成员变量)进行查询,即调用形如E method()的成员方法,最好使用? extends E,当然也可以使用?? super E,只是需要自行强转类型。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。