一, 问题产生
今天在学习 ArrayList 源码的时候发现了这么一句注释, 即:
c.toArray might (incorrectly) not return Object[] (see 6260652)
这句话的意思是 Collection 集合类型的 toArray()方法虽然声明返回值类型是 Object[], 但是具体调用时还真不一定就返回 Onject[]类型, 也有可能是其他的类型, 这还要取决于你 c 的实际类型, 使用不当还会抛出异常. 这样讲可能会很懵比, 下面我将会详细讲解到底为什么, 现在我们先来看看 Collection 中的 toArray()声明, 让你对这个方法先有个大概的印象.
public Object[] toArray(); // 声明返回值类型为 Object[]
那么什么情况会出现上面的 bug 呢? 我们先来看看下面两个例子:
1, 没有抛异常的情况
- // 声明一个 ArrayList 集合, 泛型为 String 类型
- List<String> list = new ArrayList<>();
- // 添加一个元素
- list.add("list");
- // 将上面的集合转换为对象数组
- Object[] listArray = list.toArray(); ................ 1
- // 输出 listArray 的类型, 输出 class [Ljava.lang.Object;
- System.out.println(listArray.getClass());
- // 往 listArray 赋值一个 Onject 类型的对象
- listArray[0] = new Object();
2, 抛异常的情况
- // 同一创建一个列表, 但是现在是通过 Arrays 工具类来创建, 创建的列表类型为 Arrays 的内部类 ArrayList 类型
- List<String> asList = Arrays.asList("string");
- // 转换为对象数组
- Object[] asListArray = asList.toArray();.............. 2
- // 输出转换后元素类型, 将输出 class [Ljava.lang.String;
- System.out.println(asListArray.getClass());
- // 往对象数组中添加 Object 类型对象, 会报错 java.lang.ArrayStoreException
- asListArray[0] = new Object();
上面第一种情况是通过 new ArrayList()方式创建的
java.util.ArrayList
类型, 第二种方式是使用 Arrays.asList()方式创建的
java.util.Arrays$ArrayList
的类型, 两个类型名都是 ArrayList, 但实现方式确实不同的. 那为什么会报错呢? 归根到底就是 toArray()这个方法的实现方式不同导致的. 我们分别先看下
java.util.ArrayList
类的 toArray()和
java.util.Arrays$ArrayList
的 toArray()的实现方式:
- java.util.ArrayList
- public Object[] toArray() {
- // 调用 Arrays 工具类进行数组拷贝
- return Arrays.copyOf(elementData, size);...............1
- }
- Arrays.copyOf()
- public static <T> T[] copyOf(T[] original, int newLength) {
- return (T[]) copyOf(original, newLength, original.getClass());.................2
- }
- 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;
- }
下面是
java.util.Arrays$ArrayList
的实现
- private final E[] a;
- @Override
- public Object[] toArray() {
- return a.clone();
- }
从上面可以看出, 在
java.util.ArrayList
中将会调用 Arrays 的 copyOf()方法, 传入一个 newType 进行类型判断, newType 的值为
original.getClass()
, 由于
original.getClass()
返回的类型为 Object[](具体看 ArrayList 的源码可知), 所以调用 toArray()之后将返回一个 Object[]类型数组, 所以往 listArray 变量里边丢一个 Object 类型的对象当然不会报错.
再看看
java.util.Arrays$ArrayList
的实现, 可以看出数据类型定义为泛型 E,E 的具体类型将根据你传入的实际类型决定, 由于你传入了 "string", 所以实际类型就是 String[], 当然调用 a.clone()之后还是一样返回 String[]类型, 只不过是这里做了一个向上转型, 将 String[]类型转为 Object[]类型返回罢了, 但是注意, 虽然返回的引用为 Object[], 但实际的类型还是 String[], 当你往一个引用类型和实际类型不匹配的对象中添加元素时, 就是报错. 不服? 下面举个栗子:
- // 数组 strings 为 String[]类型
- String[] strings = { "a", "b" };
- // 向上转型为 Object[]类型, 那么这个 objects 就属于引用类型为 Object[], 而实际类型为 String[]
- Object[] objects = strings;
- // 添加一个 Object 类型变量, 就报错啦!
- objects[0] = new Object();//! java.lang.ArrayStoreException
为了加深理解, 我们来总结下 java 中的向上转型和向下转型的区别. 我们都知道我们可以通过注入
Father fa = new Son()
的方式进行声明, 仅为 Father 类型为 Son 类型的父类, 即发生向上转型, 向上转型在 java 中是自动完成的, 不需要进行强制转换, 不会抛出异常. 向下转型分为两种情况, 下面结合代码演示:
- // 向上转型
- Father fa = new Son();
- Father fafa = new Father();
- // 向下转型(不会报错)
- Son son = (Son) fa;.................1
- // 向下转型, 报错了 java.lang.ClassCastException
- Son sonson = (Son) fafa;.......................2
可以发现 1 处不会报错, 2 处却报错了, 因为 1 处 fa 变量的实际类型是 Son, 引用类型为 Father, 向下转换取决于实际类型而不取决于引用类型, 比如 fafa 这个变量的实际类型就是其本身 Father, 在 java 中, 父类默认是不能强制转换为子类的.
二, 总结
首先最重要有以下几点:
1,Java 中数组集合向上转型之后, 不能往数组集合中添加引用类型 (即父类型) 的对象, 而应该添加实际类型的对象, 比如说 ``Father[] father = son[], 你就不能往 father 中添加 Father
类型了, 而应该是
Son`
2,Java 中向上转型是默认允许的, 但是向下转型可能会抛出错误, 得小心使用!
3, 要小心采用 Arrays.asList()创建的集合类型不是
java.util.ArrayList
, 而是
java.util.Arrays$ArrayList
, 两个类的很多方法实现方式也不一样.
谢谢阅读, 欢迎评论区交流!
来源: https://juejin.im/post/5b2b3c06e51d4558c6520b63