Java中的协变和逆变

本文主要分成以下几个部分:、

  • 回顾在简明数据结构中的异常
  • 介绍Java中的协变和逆变的概念

JDK中的ArrayList的异常

 public ArrayList(Collection c) {
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            // c.toArray might (incorrectly) not return Object[] (see 6260652) ****
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // replace with empty array.
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

之前的文章中只说明了问题存在的一个方面(方法继承和重写方面的问题),其实这个问题存在的根本原因是Java中的数组是协变的。

协变和逆变的概念介绍

数组是协变的!

如果AB的子类,规定f()是一种“变化”,比如强制类型转换,使用集合类的泛型,数组等等,若f(A)f(B)的子类型,我们成A可以协变为B,否则就是逆变;如果这样的变化后f(A)f(B)不能构成父子类型关系,我们称之为不变。
协变在Java中的使用场景非常多,数组就是协变的,请看:

 Object objects[] = new Integer[20];

我们可以很容易的将Integer类型的数组变成Object类型的数组,这两种数组看起来就满足了一种父类型和子类型的关系,这就是协变。但是这样的“变化”( f() )是具有风险的:

 Object objects[] = new Integer[20];
 objects[0] = "damn!";

Exception in thread "main" java.lang.ArrayStoreException: java.lang.String
    at Main.main(Main.java:19)

objects 这个句柄实际上指向的是一个Integer数组的引用,因此,不能存储一个String类型的字面量。上面的代码虽然可以过编译,但是在运行期会报错。

泛型是不变的?

 ArrayList list = new ArrayList();
 
 

很明显ObjectString类型的父类型,但是这样的代码在编译期间都不能成立。所以泛型不是协变的。

但是可以用通配符和泛型上下界的方法让泛型具有协变和逆变的性质。

//协变
 ArrayList< ? extends  Number> list= new ArrayList();
//逆变
ArrayList< ? super Integer> list2 = new ArrayList();
list.add(1); //error 

这里的最后一行是错误的,直接会在编译期报错,这是因为通配符的语义是表示Number的任意一种子类,可能是Float Double等等,所以放入Integer类型可能导致类型转换错误。这里和Object类型的语义完全不同之处在于这里没有执行一个向下转型操作。

  ArrayList list3 = new ArrayList<>();
  list3.add(1);
 
 

因此,使用上边界标识符extends的集合只能读取集合中的信息:

ArrayList list1 ;
ArrayList list = new ArrayList<>(); list.add(1);
list1 = list;
list1.get(0);

相反下边界标识符super只能向集合中存储东西,因为读取出来的内容向下转型是存在风险的

 ArrayList list3 = new ArrayList<>();
  list3.add(0);
 Integer i = list3.get(0);

JDK中的这个Bug的解释

因为Java中的数组是协变的,所以真正存储的类型不一定是Object数组,所以再向数组中存储内容是存在风险的。

你可能感兴趣的:(Java中的协变和逆变)