Java 泛型的不变性 (invariance)协变性 (covariance)逆变性 (contravariance)

Posted 古月书斋

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java 泛型的不变性 (invariance)协变性 (covariance)逆变性 (contravariance)相关的知识,希望对你有一定的参考价值。

本文整理自:https://chiclaim.blog.csdn.net/article/details/85575213

我们先定义三个类:Plate、Food、Fruit

//定义一个`盘子`类
public class Plate<T> 

    private T item;

    public Plate(T t) 
        item = t;
    

    public void set(T t) 
        item = t;
    

    public T get() 
        return item;
    



//食物
public class Food 



//水果类
public class Fruit extends Food 


然后定义一个takeFruit()方法

private static void takeFruit(Plate<Fruit> plate) 

1、不变性 (invariance)

调用takeFruit方法,尝试把一个装着苹果的盘子传进去:

takeFruit(new Plate<Apple>(new Apple())); //泛型之不变

发现编译器报错,发现装着苹果的盘子竟然不能赋值给装着水果的盘子,这就是泛型的不变性 (invariance)

这个时候就要引出泛型的协变性

2、协变性

假设我就要把一个装着苹果的盘子赋值给一个装着水果的盘子呢?

我们来修改下 takeFruit 方法的参数 (? extends Fruit):

private static void takeFruit(Plate<? extends Fruit> plate) 

然后调用 takeFruit 方法,把一个装着苹果的盘子传进去:

takeFruit(new Plate<Apple>(new Apple())); //泛型的协变

这个时候编译器不报错了,而且你不仅可以把装着苹果的盘子放进去,还可以把任何继承了 Fruit 类的水果都能放进去:

//包括自己本身 Fruit 也可以放进去
takeFruit(new Plate<Fruit>(new Fruit()));
takeFruit(new Plate<Apple>(new Apple()));
takeFruit(new Plate<Pear>(new Pear()));
takeFruit(new Plate<Banana>(new Banana()));

在 Java 中把 ? extends Type 类似这样的泛型,称之为 上界通配符(Upper Bounds Wildcards)

为什么叫上界通配符?因为 Plate<? extends Fruit>,可以存放 Fruit 和它的子类们,最高到 Fruit 类为止。所以叫上界通配符

好,现在编译器不报错了,我们来看下 takeFruit 方法体里的一些细节:
 

private static void takeFruit(Plate<? extends Fruit> plate) 
    //plate5.set(new Fruit());    //编译报错
    //plate5.set(new Apple());    //编译报错
    Fruit fruit = plate5.get();   //编译正常

发现 takeFruit() 的参数 plate 的 set 方法不能使用了,只有 get 方法可以使用。

为什么参数 plate 的 set 方法不能使用了?因为Plate<? extends Fruit> plate这里说明了plate中适配的是Fruit类和它的子类们;而即使我们通过set 方法设置的也是Fruit的子类,但是其实并不能保证他们之间是兼容的。

如果我们需要调用 set 方法呢?这个时候就需要引入泛型的逆变性

3、逆变性

修改下泛型的形式 (extends 改成 super):

private static void takeFruit(Plate<? super Fruit> plate)
    plate.set(new Apple());     //编译正常
    //Fruit fruit = plate.get(); //编译报错
    //Fruit pear = plate.get();   //编译报错

发现 set 方法可以用了,但是 get 方法“失效”了。我们把类似 ? super Type 这样的泛型,称之为下界通配符(Lower Bounds Wildcards)

为什么参数 plate 的 get 方法不能使用了?因为Plate<? super Fruit> plate这里说明了plate中适配的是Fruit类和它的父类;我们通过get 方法返回的当然不一定是Fruit类,自然就不能使用啦!

我们可以简单理解为上界通配符的泛型存放(适配)的是该类型的子类们。

下界通配符 (super) 存放(适配) 该类型和它的父类们。所以对于 Plate<? super Fruit> 只能放进 Fruit 和 Food。

4、小结

(1)、上界通配符的泛型存放(适配) 的是该类型的和它的子类们下界通配符存放(适配) 的是该类型和它的父类们。

(2)、PECS(Producer Extends, Consumer Super)

上界通配符一般用于读取,下界通配符一般用于修改。比如 Java 中 Collections.java 的 copy 方法:

public static <T> void copy(List<? super T> dest, List<? extends T> src) 
    int srcSize = src.size();
    if (srcSize > dest.size())
        throw new IndexOutOfBoundsException("Source does not fit in dest");

    if (srcSize < COPY_THRESHOLD ||
        (src instanceof RandomAccess && dest instanceof RandomAccess)) 
        for (int i=0; i<srcSize; i++)
            dest.set(i, src.get(i));
     else 
        ListIterator<? super T> di=dest.listIterator();
        ListIterator<? extends T> si=src.listIterator();
        for (int i=0; i<srcSize; i++) 
            di.next();
            di.set(si.next());
        
    


dest 参数只用于修改,src 参数用于读取操作,只读 (read-only)

通过泛型的协变逆变来控制集合是只读,还是只改。使得程序代码更加优雅。

以上是关于Java 泛型的不变性 (invariance)协变性 (covariance)逆变性 (contravariance)的主要内容,如果未能解决你的问题,请参考以下文章

c#中关于协变性和逆变性(又叫抗变)详解

Kotlin泛型 ③ ( 泛型 out 协变 | 泛型 in 逆变 | 泛型 invariant 不变 | 泛型逆变协变代码示例 | 使用 reified 关键字检查泛型参数类型 )

C#学习笔记8

TypeScript 中泛型的不安全隐式转换

Scala之旅-变性

进入快速通道的委托(深入理解c#)