今天写代码遇到一个奇怪的问题,代码结构如下:

ArrayList<String> list = new ArrayList<String>();
String strings[] = (String[]) list.toArray();

这样写代码个人觉得应该没什么问题,编译也没有问题。

可是具体运行的时候报异常,如下:

Exception in thread "main" java.lang.ClassCastException: [Ljava.lang.Object;

 

但是如果这样写就没有问题:

ArrayList<String> list = new ArrayList<String>();
int len = list.size();
String strings[] = new String[len];
for(int i = 0, j = list.size(); i < j; i++){
	strings[i]=list.get(i);
}

 

这个问题怎么解释呢?

Java中允许向上和向下转型,但是这个转型是否成功是根据Java虚拟机中这个对象的类型来实现的。

Java虚拟机中保存了每个对象的类型,而数组也是一个对象。

数组的类型是[Ljava.lang.Object,把[Ljava.lang.Object转换成[Ljava.lang.String是显然不可能的事情,因为这是一个向下转型,而虚拟机只保存了这是一个Object的数组,不能保证数组中的元素是String的,所以这个转型不能成功。数组里面的元素只是元素的引用,不是存储的具体元素,所以数组中元素的类型还是保存在Java虚拟机中的。

根据上面的解释,我们可以把这个问题归纳到下面这个模型。

Object objs[] = new Object[10];
String strs[] = (String[])objs;

这样子和刚才上面编译错误是一样的,

如果我们把修改一下这个代码,如下:

String strs[] = new String[10];
Object objs[] = strs;

这样子就可以编译通过了,所以这个问题我们可以归结为一个Java转型规则的一个问题。

 

Java数组对范型的支持问题:

JDK5中,已经有了对泛型的支持,这样可以保证在集合和Map中的数据类型的安全,可是List的toArray方法返回的竟然是Object []让我很迷惑。个人感觉应该可以根据范型,直接返回相应的T []。

仔细看了一下JDK的源码发现List转化为array有两个方法:

public Object[] toArray();

这个方法把List中的全部元素返回一个相同大小的数组,数组中的所有元素都为Object类型。

public <T> T[] toArray(T[] a);

这个方法把List中的全部元素返回一个相同大小的数组,数组中的所有元素都为T类型。

List如此设计是因为Java编译器不允许我们new范型数组,也就是说你不能这么定义一个数组:

T arr=new T[size];

但是你却可以用T[]来表示数组,而且可以把数组强制转化为T[]的。

比如List中的public T[] toArray(T[] a)是这么实现的:

public <T> T[] toArray(T[] a) {
	if (a.length < size) {
		a = (T[]) java.lang.reflect.Array.newInstance(a.getClass().getComponentType(), size);
	}
	System.arraycopy(elementData, 0, a, 0, size);
	if (a.length > size) {
		a[size] = null;
	}
	return a;	
}

从上面代码中可以看到,你必须通过反射来创建这个数组,因为你不知道这个数组的类型。

a.getClass().getComponentType()方法是取得一个数组元素的类型。

 

Java为什么不支持创建范型数组?

我想这个问题的答案是:这样做会破坏类型安全,其核心的问题在于Java范型和C#范型存在根本区别:

Java的范型停留在编译这一层,到了运行时,这些范型的信息其实是被抹掉的;

而C#的范型做到了MSIL(Microsoft Intermediate Language,微软中间语言)这一层。

Java的做法不必修改JVM,减少了潜在的大幅改动和随之而来的风险,也许同时也反映出Java Bytecode规范在设计之初的先天不足;

C#则大刀阔斧,连CLR(Common Language Runtime,公共语言运行时)一起改以支持更彻底的范型,换句话说,在范型这一点上,感觉C#更像C++。

在Java中,Object[]数组可以是任何数组的父类,或者说,任何一个数组都可以向上转型成它在定义时指定元素类型的父类的数组,这个时候如果我们往里面放不同于原始数据类型,但是满足后来使用的父类类型的话,编译不会有问题,但是在运行时会检查加入数组的对象的类型,于是会抛ArrayStoreException:

String[] strArray = new String[20];
Object[] objArray = strArray;
objArray[0] = new Integer(1);  // throws ArrayStoreException at runtime

因为Java的范型会在编译后将类型信息抹掉,如果Java允许我们使用类似:

Map<Integer, String>[] mapArray = new Map[20];

这样的语句的话,我们在随后的代码中可以把它转型为Object[],然后往里面放Map<Double, String>实例。

这样做不但编译器不能发现类型错误,就连运行时的数组存储检查对它也无能为力,它能看到的是我们往里面放Map的对象,我们定义的<Integer, String>在这个时候已经被抹掉了,于是而对它而言,只要是Map,都是合法的。想想看,我们本来定义的是装Map<Integer, String>的数组,结果我们却可以往里面放任何Map(如:Map<Double, String>),接下来如果有代码试图按原有的定义去取值,后果是什么不言自明。

所以,Java编译器不允许我们new范型数组

 

toArray() 两种实现方式

public Object[] toArray() {
	Object[] a = c.toArray();
	for (int i=0; i<a.length; i++) {
		a[i] = new UnmodifiableEntry<>((Map.Entry)a[i]);
	}
	return a;
}

public <T> T[] toArray(T[] a) {
	// We don't pass a to c.toArray, to avoid window of
	// vulnerability wherein an unscrupulous multithreaded client
	// could get his hands on raw (unwrapped) Entries from c.

	Object[] arr = c.toArray(a.length==0 ? a : Arrays.copyOf(a, 0));

	for (int i=0; i<arr.length; i++) {
		arr[i] = new UnmodifiableEntry<>((Map.Entry)arr[i]);
	}

	if (arr.length > a.length) {
		return (T[])arr;
	}

	System.arraycopy(arr, 0, a, 0, arr.length);

	if (a.length > arr.length) {
		a[arr.length] = null;
	}
	
	return a;
}

toArray() 源码,请参见我在google code 上传的 sdk 源码:  src-jdk1.7.0_02

 

原文: Java数组转型和范型

 

 

参考推荐:

Java之Array数组

Java数组转型和范型

Java 技术面试题及答案

Java int 和 Integer 区别

Java 实现链表类 LinkList

Java list 用法及排序和遍历

Java map 用法及排序和遍历

Java JDK升级各个版本的新特性