问题现象
今天偶然看到了一个 JDK 的 Bug,给大家分享一下。
假设现在有如下的代码:
List<String> list = new ArrayList<>();
list.add("1");
Object[] array = list.toArray();
array[0] = 1;
System.out.println(Arrays.toString(array));
上面的代码是可以正常支执行的,如下图所示:
修改代码为如下代码:
List<String> list = Arrays.asList("1");
Object[] array = list.toArray();
array[0] = 1;
System.out.println(Arrays.toString(array));
再次执行代码,结果就会抛出 ArrayStoreException
异常,这个异常表明这里并不能把一个 Integer
类型的对象存放到这个数组里面。如下图所示:
查看 Arrays
的静态内部类 ArrayList
的 toArray()
方法的返回值就是 Object[]
类型的,如下图所示:
这里就会引发一个疑问: 为啥使用 java.lang.util.ArrayList
代码就可以正常运行?但是使用 Arrays
的静态内部类 ArrayList
就会报错了?
原因分析
首先看下 java.lang.util.ArrayList
类的 toArray()
方法的实现逻辑:
从上面可以看出 toArray()
方法是拷贝了一个 ArrayList
内部的数组对象,然后返回的。而 elementData
这个数组在实际初始化的时候,就是 new 了 Object
类型的数组。如下图所示:
那么经过拷贝之后返回的还是一个实际类型为Object
类型的数组。既然这里是一个 Object
类型的数组,那么往里面放一个 Integer
类型的数据是合法的,因为 Object
是 Integer
类型的父类。
然后再看下 Arrays
的静态内部类 ArrayList
的 toArray()
方法的实现逻辑。这里返回的是 a
这个数组的一个克隆。如下图所示:
而这个 a
数组声明的类型是 E[]
,根据泛型擦除后的原则,这里实际上声明的类型也变成了 Object[]
。 如下图所示:
那接下来再看看 a
实际的类型是什么? 由于 Arrays
的静态内部类 ArrayList
的构造函数是包级访问的,因此只能通过 Arrays.asList()
静态方法来构造一个这个对象。如下图所示:
而 Arrays.asList()
方法的签名是变长参数类型,这个是 Java 的一个语法糖,实际对应的是一个数组,泛型擦除后就变成了 Object[]
类型。如下图所示:
而在代码实际调用处,实际上会 new
一个 String
类型的数组,也就是说 「a
的实际类型是一个 String
类型的数组」。 那么 a 调用了 clone()
方法之后返回的类型也是一个 String 类型的数组,克隆嘛,类型一样才叫克隆。如下图所示:
经过上面的分析,答案就呼之欲出了。a
的实际类型是一个 String
类型的数组,那么往这个数组里面放一个 Integer
类型的对象那肯定是要报错的。等效代码如下图所示:
为什么是个Bug ?
查看 Collection
接口的方法签名,方法声明明确是要返回的是一个 Object[]
类型的数组,因为方法明确声明了返回的是一个 Object[]
类型的数组,但是实际上在获取到了这个返回值后把它当作一个 Object[]
类型的数组使用某些情况下是不满足语义的。
同时这里要注意一下,返回的这个数组要是一个 「安全」的数组,安全的意思就是「集合本身不能持有对返回的数组的引用」,即使集合的内部是用数组实现的,也不能直接把这个内部的数组直接返回。这就是为什么上面两个 toArray()
方法的实现要么是把原有的数组复制了一份,要么是克隆了一份,本质上都是新建了一个数组。如下图所示:
在 OpenJDK 的 BugList 官网上很早就有人提出这个问题了,从时间上看至少在 2005 年就已经发现这个 Bug 了,这个 Bug 真正被解决是在 2015 年的时候,整整隔了 10 年时间。花了 10 年时间修这个 Bug,真是十年磨一剑啊!
如何修正的这个 Bug ?
JDK 9 中的实现修改为了新建一个 Object
类型的数组,然后把原有数组中的元素拷贝到这个数组里面,然后返回这个 Object
类型的数组,这样的话就和 java.util.ArrayList
类中的实现方法一样了。
在 java.util.ArrayList
类的入参为 Collection\<? exends E>
类型的构造函数中就涉及到可能调用 Arrays
的静态内部类 ArrayList
的 toArray()
方法,JDK 在实现的时候针对这个 Bug 还做了特殊的处理,不同厂商发行的 JDK 处理方式还有细微的不同。
Oracel JDK 8 版本的实现方式:
Eclipse Temurin Open JDK 8 版本的实现方式:
之所以在 java.util.ArrayList
对这个 Bug 做特殊的处理是因为 Sun 公司在当时选择不修复改这个Bug,因为怕修复了之后已有的代码就不能运行了。如下图所示:
比如在修复前有如下的代码,这个代码在 JDK 8 版本是可以正常运行的,如下图所示:
String[] strings = (String[]) Arrays.asList("foo", "bar").toArray();
for (String string : strings) {
System.out.println(string);
}
但是如果升级到 JDK 9 版本,就会报 ClassCastException
异常了,如下图所示:
因为修复了这个 Bug 之后,编译器并不能告诉你原来的代码存在问题,甚至连新的警告都没有。假设你从 JDK 8 升级到 JDK 9 了,代码也没有改,但是突然功能就用不了,这个时候你想不想骂人,哈哈哈哈。这也许就是 Sun 公司当年不愿意修复这个 Bug 的原因之一了。当然,如果你要问我为什么要升级的话,我会说:你发任你发,我用 Java 8 !
题外话
阿里巴巴的 Java开发手册对 toArray(T[] array)
方法的调用有如下的建议:
这里以 java.util.ArrayList
类的源码作为参考,源码实现如下:
// ArrayList 的 toArray() 方法实现:
public <T> T[] toArray(T[] a) {
if (a.length < size) // 如果传入的数组的长度小于 size
// Make a new array of a's runtime type, but my contents:
return (T[]) Arrays.copyOf(elementData, size, a.getClass());
System.arraycopy(elementData, 0, a, 0, size);
if (a.length > size)
a[size] = null;
return a;
}
// Arrays 的 coypyOf 方法实现:
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
@SuppressWarnings("unchecked")
T[] copy = ((Object)newType == (Object)Object[].class)
? (T[]) new Object[newLength]
: (T[]) Array.newInstance(newType.getComponentType(), newLength);
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
当调用 toArray()
方法时传入的数组长度为 0 时,方法内部会根据传入的数组类型动态创建一个和当前集合 size 相同的数组,然后把集合的元素复制到这个数组里面,然后返回。
当调用 toArray()
方法时传入的数组长度大于 0,小于 ArrayList
的 size 时,走的逻辑和上面是一样的,也会进入到 Arays
的 copyOf
方法的调用中,但是调用方法传入的新建的数组相当于新建之后没有被使用,白白浪费了,需要等待 GC 回收。
当调用 toArray()
方法时传入的数组长度大于等于 ArrayList
的 size 时,则会直接把集合的元素拷贝到这个数组中。如果是大于的情况,还会把数组中下标为 size
的元素设置为 null,但是 size
下标后面的元素保持不变。如下所示:
List<String> list = new ArrayList<>();
list.add("1");
String[] array = new String[3];
array[1] = "2";
array[2] = "3";
String[] toArray = list.toArray(array);
System.out.println(array == toArray);
System.out.println(Arrays.toString(toArray));
手册中提到的在高并发的情况下,传入的数组长度等于 ArrayList
的 size 时,如果 ArrayList 的 size 在数组创建完成后变大了,还是会走到重新新建数组的逻辑里面,仍然会导致调用方法传入的新建的数组没有被使用,而且这里因为调用方法时新建的数组和 ArrayList
之前的 size 相同,会造成比传入长度为 0 的数组浪费多得多的空间。但是我个人觉得,因为 ArrayList
不是线程安全的,如果存在数据竞争的情况就不应该使用。
参考
Arrays.asList(x).toArray().getClass() should be Object[].class
array cast Java 8 vs Java 9
toArray方法的小陷阱,写开发手册的大佬也未能幸免
.toArray(new MyClass[0]) or .toArray(new MyClass[myList.size()])?
Arrays of Wisdom of the Ancients
Java开发手册(黄山版).pdf