java中关于协变和逆变的实现

介绍

协变和逆变描述的是类型转换后的继承关系。
定义A,B两个类型,A是B的子类,f(A) 表示类型转换后的类型,如List

  • 协变 A <= B,f(A) <= f(B) 成立
  • 逆变 A <= B,f(A) >= f(B) 成立
  • 不变 A <= B,都不成立

数组的协变和逆变

public class Client {

  public static void main(String[] args) {
    Fruit[] fruits = new Apple[5];
    fruits[0] = new Apple();
    fruits[1] = new Fruit();
  }

  static class Fruit {

  }

  static class Apple extends Fruit {

  }

  static class RedApple extends Apple {

  }

  static class Orange extends Fruit {

  }
}

Apple 是 Fruit 的子类,Fruit[] fruits = new Apple[5] 成立,说明数组是协变的。上面的程序在运行时会抛出异常 ArrayStoreException,这是因为数组的实际类型是 Apple,但实际存放的类型为 Fruit。

容器的协变和逆变

public class Client2 {

  public static void main(String[] args) {
    //编译报错
    List<Fruit> fruitList = new ArrayList<Apple>();
    //编译报错
    List<Apple> appleList = new ArrayList<Fruit>();
  }
}

可以看出泛型既不是协变的,也不是逆变的,而是不变的,但这就相当于放水果的盘子不能放苹果一样,感觉很别扭,所以java引入了通配符来支持协变和逆变。

public class Client2 {

  public static void main(String[] args) {
    List<? extends Fruit> fruitList = new ArrayList<>();
    //编译报错
    fruitList.add(new Apple());
    //编译报错
    fruitList.add(new Fruit());
    Fruit fruit = fruitList.get(0);
  }
}

使用<? extends Fruit> 来支持协变,表示Fruit及子类,但编译器不能实际确定容器中元素的类型,可能是 Fruit,可能是 Apple,编译器不能确定要添加的元素是否和容器中元素是否能匹配,所以就不允许添加了,只能获取。(这可能是一个放苹果的盘子,也可能是放橙子的盘子,这个时候苹果和橙子都不能往里放,但我们可以说取出来的一定是一个水果)。

public class Client2 {

  public static void main(String[] args) {
    //逆变
    List<? super Apple> fruitList = new ArrayList<>();
    fruitList.add(new Apple());
    fruitList.add(new RedApple());
    //编译报错
    Apple apple = fruitList.get(0);
    //编译通过
    Object obj = fruitList.get(0);
  }
}

使用<? super Apple> 来支持逆变,表示Apple及父类,但编译器不能实际确定容器中元素的类型,可能是 Apple,可能是 Fruit。编译器不能确定容器中的元素的类型,所以就不允许获取了,只能添加。(这可能是放苹果的盘子,也可能是放水果的盘子,两种盘子都可以放苹果,既然可以放苹果,肯定可以放红苹果,但我们不能确定取出来的是苹果还是红苹果,或其他水果)。

关于什么时候使用协变,逆变,《Effective Jave》有总结
producer-extends,consumer-super,简单来说就是作为生产者时使用extends,作为消费者时使用super。Collections.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作为生产者,只能取数据。

posted @ 2020-11-08 23:17  strongmore  阅读(267)  评论(0编辑  收藏  举报