Arrays.toList(T[] t)返回的List不可执行add方法
前言
首先让我们来看一段代码:
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 ? get(i)==null : 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源码内部了解了问题的根源,更让我接触到了大牛们写代码的思想:明确类的设计目的,从而明确类的功能边界,在编码的过程中绝不越界完成,也不重复完成功能。