ArrayList源码分析(基于jdk1.8)(三):Arrays.asList方法带来的问题

2020-08-11 14:49:47 浏览数 (1)

Arrays.asList,本来是另外一个类,之所以放到ArrayList相关的文章里面一并讨论,是因为这也是我们日常在使用过程中的一个误区,容易将Arrays.asList产生的结果与ArrayList进行等价。

1.问题重现

1.1 int数组转ArrayList问题

一开始,我们最简单的需求就是将一个数组转为list,搜索了很多资料之后,有人告诉你,Arrays.asList是专门解决这个问题的好办法:

代码语言:javascript复制
public static void main(String[] args) {
	int[] arr = {1,2,3,4,5};
	List list = Arrays.asList(arr);
	System.out.println(list);
}

我们以为,结果会输出“1,2,3,4,5”但是这个结果确是:

代码语言:javascript复制
[[I@1b6d3586]

这个结果让我们始料未及,究竟是哪出了问题呢?于是,对这个list中的内容进行debug:

可以发现,这个list中实际上只有一个内容,就是这个数组。asList并没有如我们想的将所有的数据的元素转换为ArrayList中的一项值。而是直接将数组变成了一个元素项。

1.2 asList之后的增删问题

对于上面章节,所提到的问题,如果不是单单纯的int数组,string数组是能够成功的。假定我们需要对这个string数组转为list之后,对这个ArrayList进行增删操作:

代码语言:javascript复制
public static void main(String[] args) {
    String[] arr1 = {"1","2","3","4","5"};
	List list2 = Arrays.asList(arr1);
	System.out.println(list2);
	list2.add("6");
}

这又产生了一个意想不到的结果:

代码语言:javascript复制
[1, 2, 3, 4, 5]
Exception in thread "main" java.lang.UnsupportedOperationException
	at java.util.AbstractList.add(AbstractList.java:148)
	at java.util.AbstractList.add(AbstractList.java:108)
	at com.dhb.ArrayList.test.ArraysAsListTest.main(ArraysAsListTest.java:21)

出现了UnsupportedOperationException异常。

1.3 asList之后修改原数组的问题

如果我们在使用asList的时候,对原始数组进行修改,那么会出现什么结果?

代码语言:javascript复制
public static void main(String[] args) {
    
    String[] arr1 = {"1","2","3","4","5"};
    List list2 = Arrays.asList(arr1);
    System.out.println(list2);
    arr1[1] = "0";
    System.out.println(list2);
}

输出为:

代码语言:javascript复制
[1, 2, 3, 4, 5]
[1, 0, 3, 4, 5]

可以看到,当我们使用asList之后,再对其原始的数组进行修改,那么之前被转换的List的值也会发生变化。如果我们把这个结果通过参数传递给其他线程,那么可能就会产生很多共享数据导致的奇怪问题。

2.源码分析

2.1 asList方法

我们打开Arrays源码,看看asList方法:

代码语言:javascript复制
  /**
     * Returns a fixed-size list backed by the specified array.  (Changes to
     * the returned list "write through" to the array.)  This method acts
     * as bridge between array-based and collection-based APIs, in
     * combination with {@link Collection#toArray}.  The returned list is
     * serializable and implements {@link RandomAccess}.
     *
     * <p>This method also provides a convenient way to create a fixed-size
     * list initialized to contain several elements:
     * <pre>
     *     List&lt;String&gt; stooges = Arrays.asList("Larry", "Moe", "Curly");
     * </pre>
     *
     * @param <T> the class of the objects in the array
     * @param a the array by which the list will be backed
     * @return a list view of the specified array
     */
    @SafeVarargs
    @SuppressWarnings("varargs")
    public static <T> List<T> asList(T... a) {
        return new ArrayList<>(a);
    }

可以看到,asList方法实际上是一个泛型方法,支持传入的是泛型对象。前面我们讨论过泛型的擦除原理,泛型只支持对象。那么对于int数组,由于其元素都是int型,那么泛型是不支持的。那么只有数组才是对象,因此就不难理解为什么前面的int数组被转换成了一个只有一个元素的List,这个元素就是数组本身。 对于这个问题,更好的解决办法是,在jdk1.8之后出现了Stream的装箱的方法:

代码语言:javascript复制
public static void main(String[] args) {
	int[] arr = {1,2,3,4,5};
	List list = Arrays.asList(arr);
	System.out.println(list);

	List list1 = Arrays.stream(arr).boxed().collect(Collectors.toList());
	System.out.println(list1);
}

输出结果:

代码语言:javascript复制
[[I@1b6d3586]
[1, 2, 3, 4, 5]

这样通过Stream的bixed方法就很好的解决了这个问题。

2.2 ArrayList内部类

在Arrays的asList方法中,虽然new了一个ArrayList,但是需要注意的是,这个ArrayList并非我们一直使用的ArrayList,而是一个位于Arrays中的内部类:

代码语言:javascript复制
   /**
     * @serial include
     */
    private static class ArrayList<E> extends AbstractList<E>
        implements RandomAccess, java.io.Serializable
    {
        private static final long serialVersionUID = -2764017481108945198L;
        private final E[] a;

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

        @Override
        public int size() {
            return a.length;
        }

        @Override
        public Object[] toArray() {
            return a.clone();
        }

        @Override
        @SuppressWarnings("unchecked")
        public <T> T[] toArray(T[] a) {
            int size = size();
            if (a.length < size)
                return Arrays.copyOf(this.a, size,
                                     (Class<? extends T[]>) a.getClass());
            System.arraycopy(this.a, 0, a, 0, size);
            if (a.length > size)
                a[size] = null;
            return a;
        }

        @Override
        public E get(int index) {
            return a[index];
        }

        @Override
        public E set(int index, E element) {
            E oldValue = a[index];
            a[index] = element;
            return oldValue;
        }

        @Override
        public int indexOf(Object o) {
            E[] a = this.a;
            if (o == null) {
                for (int i = 0; i < a.length; i  )
                    if (a[i] == null)
                        return i;
            } else {
                for (int i = 0; i < a.length; i  )
                    if (o.equals(a[i]))
                        return i;
            }
            return -1;
        }

        @Override
        public boolean contains(Object o) {
            return indexOf(o) != -1;
        }

        @Override
        public Spliterator<E> spliterator() {
            return Spliterators.spliterator(a, Spliterator.ORDERED);
        }

        @Override
        public void forEach(Consumer<? super E> action) {
            Objects.requireNonNull(action);
            for (E e : a) {
                action.accept(e);
            }
        }

        @Override
        public void replaceAll(UnaryOperator<E> operator) {
            Objects.requireNonNull(operator);
            E[] a = this.a;
            for (int i = 0; i < a.length; i  ) {
                a[i] = operator.apply(a[i]);
            }
        }

        @Override
        public void sort(Comparator<? super E> c) {
            Arrays.sort(a, c);
        }
    }

通过这个源码我们可以发现,这个类继承了AbstractList,其所有的get和set操作都是对原有数组进行修改。而且没有add和remove等会修改数组大小的方法。

代码语言:javascript复制
String[] arr1 = {"1","2","3","4","5"};
List list2 = Arrays.asList(arr1);
System.out.println(list2);
arr1[1] = "0";
System.out.println(list2);

list2.set(1,"7");
System.out.println(list2);
System.out.println(arr1[1]);

执行上述代码,可以看到输出为:

代码语言:javascript复制
[1, 2, 3, 4, 5]
[1, 0, 3, 4, 5]
[1, 7, 3, 4, 5]
7

生成的ArrayList,实际上是原始数组的视图,这有点类似我们上一篇对ArrayList中subList方法的分析,都是一个视图功能的内部类。其get,set方法只要修改实际上是修改的原始数组,同理,修改原始数组,ArrayList的内容也会发生改变。这也不难理解为什么会出现UnsupportedOperationException异常。因为这个ArrayList本身并没有add和remove等方法。这个方法是通过抽象类实现的,而在抽象类中:

代码语言:javascript复制
 /**
     * {@inheritDoc}
     *
     * <p>This implementation always throws an
     * {@code UnsupportedOperationException}.
     *
     * @throws UnsupportedOperationException {@inheritDoc}
     * @throws ClassCastException            {@inheritDoc}
     * @throws NullPointerException          {@inheritDoc}
     * @throws IllegalArgumentException      {@inheritDoc}
     * @throws IndexOutOfBoundsException     {@inheritDoc}
     */
    public void add(int index, E element) {
        throw new UnsupportedOperationException();
    }

    /**
     * {@inheritDoc}
     *
     * <p>This implementation always throws an
     * {@code UnsupportedOperationException}.
     *
     * @throws UnsupportedOperationException {@inheritDoc}
     * @throws IndexOutOfBoundsException     {@inheritDoc}
     */
    public E remove(int index) {
        throw new UnsupportedOperationException();
    }

这两个方法直接调用本来就是会抛出UnsupportedOperationException异常。 所以这也进一步解释了,为什么ArrayList还需要用AbstractList再增加一层继承。这样可以很好的解耦。

2.3 **规范

对于上面这个问题,**巴巴的开发规范中也有描述:

禁止再对Arrays.asList产生的集合再进行add、remove、clear操作。这会导致UnsupportedOperationException异常。

3.总结

虽然此ArrayList并不是彼ArrayList,但是这也是我们容易混淆的地方,因此,对于Arrays.asList方法。我们需要注意的是:

  • 1.非对象数组,基本类型的数组不能使用Arrays.asList转换。如果需要使用,请用Stream中的boxed方法装箱之后再操作。
  • 2.asList产生的ArrayList实际上是Arrays的一个新的内部类。并不是我们认为的ArrayList,这也是大多数人容易混淆的地方。二者并不等价。
  • 3.asList产生的ArrayList实际上是对原始数组的一个视图,如果ArrayList进行set修改,那么原始数组的值也会改变。反之对原始数组进行修改,那么ArrayList的值也会改变。因此在我们将asList之后的结果再作为变量进行传递时,要特别注意这一点。
  • 4.Arrays.asList产生的ArrayList没有实现add、remove、clear方法,如果调用这些方法会出现UnsupportedOperationException异常。

上述是对Arrays.asList的总结,实际上这是很多人在编码的过程中,从来没有考虑过的问题。在面试过程中,实际上可能并不需要聊到HashMap,这ArrayList阶段就有很多人可能阵亡了。

0 人点赞