本文主要分成以下几个部分:、
- 回顾在简明数据结构中的异常
- 介绍Java中的协变和逆变的概念
JDK中的ArrayList
的异常
public ArrayList(Collection<? extends E> 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中的数组是协变的。
协变和逆变的概念介绍
数组是协变的!
如果A
是 B
的子类,规定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<Object> list = new ArrayList<String>();
很明显Object
是String
类型的父类型,但是这样的代码在编译期间都不能成立。所以泛型不是协变的。
但是可以用通配符和泛型上下界的方法让泛型具有协变和逆变的性质。
//协变
ArrayList< ? extends Number> list= new ArrayList<Integer>();
//逆变
ArrayList< ? super Integer> list2 = new ArrayList<Number>();
list.add(1); //error
这里的最后一行是错误的,直接会在编译期报错,这是因为通配符的语义是表示Number
的任意一种子类,可能是Float Double
等等,所以放入Integer
类型可能导致类型转换错误。这里和Object
类型的语义完全不同之处在于这里没有执行一个向下转型操作。
ArrayList<Object> list3 = new ArrayList<>();
list3.add(1);
因此,使用上边界标识符extends
的集合只能读取集合中的信息:
ArrayList<? extends Number> list1 ;
ArrayList<Integer> list = new ArrayList<>(); list.add(1);
list1 = list;
list1.get(0);
相反下边界标识符super
只能向集合中存储东西,因为读取出来的内容向下转型是存在风险的
ArrayList<? super Integer> list3 = new ArrayList<>();
list3.add(0);
Integer i = list3.get(0);
JDK中的这个Bug的解释
因为Java中的数组是协变的,所以真正存储的类型不一定是Object
数组,所以再向数组中存储内容是存在风险的。
网友评论