当前位置: 首页 > 工具软件 > Add To List > 使用案例 >

Arrays.toList(T[] t)返回的List不可执行add方法

公良鸿畅
2023-12-01

前言

首先让我们来看一段代码:

String[] strings = new String[]{"hello","world"};
List<String> stringList = Arrays.asList(strings);
stringList.add("java");

咋眼一看这段代码没什么问题,然而这段带却抛出了一个名为:UnsupportedOperationException的异常。看到异常时有些懵逼,查了资料说只是因为Arrays自身实现的问题。于是乎,我查看了源码。

ArrayList之李逵与李鬼

Arrays.toList(T… t) 方法返回的是Arrays的一个内部类ArrayList,大家可不要被这个名字骗了,此ArrayList非彼ArrayList啊,这完全就是李鬼啊。

声明方式

先看一下这个ArrayList的声明方式吧:

private static class ArrayList<E> extends AbstractList<E>
        implements RandomAccess, java.io.Serializable

我们经常使用的ArrayList的声明方式为:

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable

再看看AbstractList的声明方式为:

public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E>

现在可以看出我们经常使用的ArrayList相比Arrays内部的ArrayList来说多实现了一个Cloneable接口,至于为什么不给Arrays内部的ArrayList实现Cloneable接口,这与当初设计这个内部类的目的是有关系的,后面将会说到这个内部类的作用。

元素存储区

先看看Arrays.ArrayList的元素存储区的声明:

private final E[] a;

而java.util.ArrayList的存储区是这样声明的:

transient Object[] elementData;

这么一对比,李鬼假的简直太粗暴了,连一个优雅的名字都不舍得想。从声明上可以看出Arrays.ArrayList中的元素存储数组是一个不可变的数组引用,由于数组的长度本身是不可变的,所以Arrays.ArrayList从元素存储上就不支持长度变化,那么肯定是不允许add和remove操作了,所以在该类中就未对add和remove进行实现,直接调用了父类AbstractList中声明的方法:

public void add(int index, E element) {
    throw new UnsupportedOperationException();
}

public E remove(int index) {
    throw new UnsupportedOperationException();
}

public E set(int index, E element) {
    throw new UnsupportedOperationException();
}

但凡是需要使用到这三个方法的操作均会直接抛出UnsupportedOperationException异常。

java.util.ArrayList之所以可以变长,是因为使用到了Arrays.copyOf(T[] original, int newLength)方法,具体实现:

/**
 * Increases the capacity to ensure that it can hold at least the
 * number of elements specified by the minimum capacity argument.
 *
 * @param minCapacity the desired minimum capacity
 */
private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // minCapacity is usually close to size, so this is a win:
    elementData = Arrays.copyOf(elementData, newCapacity);
}

而且此类内部是实现了add、remove和set三个方法:

/**
 * Inserts the specified element at the specified position in this
 * list. Shifts the element currently at that position (if any) and
 * any subsequent elements to the right (adds one to their indices).
 *
 * @param index index at which the specified element is to be inserted
 * @param element element to be inserted
 * @throws IndexOutOfBoundsException {@inheritDoc}
 */
public void add(int index, E element) {
    rangeCheckForAdd(index);

    ensureCapacityInternal(size + 1);  // Increments modCount!!
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index);
    elementData[index] = element;
    size++;
}
/**
 * Removes the element at the specified position in this list.
 * Shifts any subsequent elements to the left (subtracts one from their
 * indices).
 *
 * @param index the index of the element to be removed
 * @return the element that was removed from the list
 * @throws IndexOutOfBoundsException {@inheritDoc}
 */
public E remove(int index) {
    rangeCheck(index);

    modCount++;
    E oldValue = elementData(index);

    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    elementData[--size] = null; // clear to let GC do its work

    return oldValue;
}
/**
 * Removes the first occurrence of the specified element from this list,
 * if it is present.  If the list does not contain the element, it is
 * unchanged.  More formally, removes the element with the lowest index
 * <tt>i</tt> such that
 * <tt>(o==null&nbsp;?&nbsp;get(i)==null&nbsp;:&nbsp;o.equals(get(i)))</tt>
 * (if such an element exists).  Returns <tt>true</tt> if this list
 * contained the specified element (or equivalently, if this list
 * changed as a result of the call).
 *
 * @param o element to be removed from this list, if present
 * @return <tt>true</tt> if this list contained the specified element
 */
public boolean remove(Object o) {
    if (o == null) {
        for (int index = 0; index < size; index++)
            if (elementData[index] == null) {
                fastRemove(index);
                return true;
            }
    } else {
        for (int index = 0; index < size; index++)
            if (o.equals(elementData[index])) {
                fastRemove(index);
                return true;
            }
    }
    return false;
}

在查看源码的过程中也许有同学已经注意到了java.util.ArrayList中使用了Arrays.copy和System.arraycopy,这两者之间的区别以后有机会再深入探讨。

到此我们也应该知道为什么Arrays.toList返回的List不支持add方法了,其根本原因是因为存储元素的数组未不可变数组。

是谁造就了李鬼?

纵观jdk中所有开放的List实现类中没有一个提供以数组为参数的构造方法。从数据结构来看,数组是一种特殊的链表(不可变长),正是因为这个特殊性,造成了在java中的数组无法使用链表(List)的一些方法,使用上与聊表对象的使用方式不同,仅支持设值、遍历与排序。再多的功能,如元素的新增、删除以及子连的获取功能均无法直接调用系统接口,只能自行编写代码。本来都是链表,却因为其中的一些特殊性,变成了两种截然不同的对象,为了打通数组与List之间的堡垒,Arrays.ArrayList诞生了,让我们可以使用更短的代码实现数组与List之间的转换。

String[] strings = new String[]{"hello","world"};
List<String> stringList = new ArrayList(Arrays.asList(strings));
stringList.add("java");

Arrays.ArrayList**仅可用于作为构造List的参数**,其他时候均可认为其就是一个数组。

那么我们上面看到了Arrays.ArrayList未实现Cloneable接口,是因为Arrays.ArrayLists的状态本身就不可变,当然其中元素可变,实现之后并无任何意义。

总结

这一个异常的抛出,让我深入jdk源码内部了解了问题的根源,更让我接触到了大牛们写代码的思想:明确类的设计目的,从而明确类的功能边界,在编码的过程中绝不越界完成,也不重复完成功能。

 类似资料: