从阿里规约谈起 - Arrays.asList 三坑

前言

将数组转换成 List 是日常开发十分常见的操作,对此 JDK 提供了一个非常好用的工具类:

1List list = Arrays.asList(array);

但是如果操纵 List 的内容的话,阿里规约会给出一个提示:

使用 asList 的问题

于是深入看了下,发现 Arrays.asList 有三个日常开发中容易坑人的地方。

坑一

如阿里规约所述,不支持 add/remove/clear 方法。

当对其使用这三个方法时,会抛出 UnsupportedOperationException 异常,如果没有捕获异常还好,程序运行崩溃,很容易测试出来,如果恰巧捕获了异常,而且直接用的 Exception 来捕获的所有异常,那么只是会造成和预期结果不符而不会崩溃,使得问题十分难以排查。

我们来看看为什么明明返回是 List,却无法使用 add/remove/clear 方法。

首先看看 asList() 源码:

1@SafeVarargs
2@SuppressWarnings("varargs")
3public static <T> List<T> asList(T... a) {
4    return new ArrayList<>(a);
5}

返回了一个 ArrayList,符合逻辑,一切都很正常。但是,众所周知 ArrayList 当然是支持 add/remove/clear 方法的,那么哪里出了问题呢?点进 ArrayList 才能发现,根本不是我们日常用的 java.util.ArrayList 而是 java.util.Arrays.ArrayList,只是 Arrays 的一个内部类。由于名称完全相同,极具迷惑性,很难发现,这个命名我认为是 JDK 一处设计败笔。

理顺了这个点,剩下的就很简单了。Arrays.ArrayList 继承自 AbstractList 这个抽象类,如下是 AbstractList 方法中 add 和 remove 方法:

1public void add(int index, E element) {
2    throw new UnsupportedOperationException();
3}
4
5public E remove(int index) {
6    throw new UnsupportedOperationException();
7}

Arrays.ArrayList 并没有具体实现 add/remove/clear 方法,所以当使用 add/remove 方法时,会直接根据 AbstractList 的代码抛出异常,clear 方法也会由于 AbstractList 实现中调用了 remove 方法而出现同样的异常。

坑二

当改变原数组的值时,List 同样发生变化。

1String[] stringData = {"a", "b", "c"};
2List stringList = Arrays.asList(stringData);
3stringData[0] = "A";
4System.out.println("stringList: " + stringList);
5//输出:stringList: [A, b, c]

根据 Arrays.ArrayList 构造函数可以看到,只是将原数组赋值给了一个内部变量,所以改变原数组的值,List 自然而然也会发生变化:

1ArrayList(E[] array) {
2    a = Objects.requireNonNull(array);
3}

坑三

不适用于基本类型。

1int[] intData = {1, 2, 3};
2List intList = Arrays.asList(intData);
3System.out.println("intList: " + intList);
4System.out.println("intListSize: " + intList.size());
5//输出:intList: [[I@7bf40ec]
6//输出:intListSize: 1

通过上面的源码可知,Arrays.asList 接受的是一个变长对象参数。基本类型并不是对象,所以按常规需要装箱操作,但是由于 int[] 本身是一个引用类型,所以 Arrays.asList 得到了一个 int[] 类型的参数,于是可以很容易得出 size 为 1。

解决方案

如果不包含基本数据类型,可以转成真正的 ArrayList。

1List list = new ArrayList<>(Arrays.asList(array));

如果是数组是基本类型的数组,需要自己写循环来转换了。

1int[] intData = {1, 2, 3};
2List intList = new ArrayList(intData.length);
3for (int intDatum : intData) {
4    intList.add(intDatum);
5}

如果开发环境能够使用 Java8 语法,可以使用 stream 操作。

1int[] intData = {1, 2, 3};
2List intList = Arrays.stream(intData).boxed().collect(Collectors.toList());