ArrayList是List接口的动态数组实现,允许存放所有元素,包括null。除了实现List接口外,ArrayList类还提供了操作数组大小的方法。功能与Vector类似,但是不同步。
类定义
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
ArrayList类继承1个抽象类,实现4个实现。
- Serializable接口:标记接口,表示可序列化。
- RandomAccess接口:标记接口,表示列表支持快速随机访问,LinkedList则不支持随机访问,没有该接口。
- Cloneable接口:标记接口,表示可复制。
- List接口:作为列表规范,规定需要实现的功能,LinkedList同样实现List接口。
- AbstractList抽象类:提供List接口骨架实现,尽可能较少实现随机访问数据所需的操作。对于连续访问数据(如链表),优先使用AbstractSequentialList,内部主要是迭代器实现。
【扩展】对于随机存储数据和连续访问数据,可以参考Collections.binarySearch()方法的源码。
// Collections
public static <T>
int binarySearch(List<? extends Comparable<? super T>> list, T key) {
if (list instanceof RandomAccess || list.size()<BINARYSEARCH_THRESHOLD)
// for循环遍历实现
return Collections.indexedBinarySearch(list, key);
else
// 迭代器实现
return Collections.iteratorBinarySearch(list, key);
}
性能比较:ArrayList用for循环遍历比iterator迭代器遍历快,LinkedList用iterator迭代器遍历比for循环遍历快!!
类注释解读
ArrayList是List接口的动态数组实现,允许存放所有元素,包括null。除了实现List接口外,ArrayList类还提供了操作数组大小的方法。功能与Vector类似,但是不同步。
时间复杂度:get、set以及迭代器操作时间复杂度是O(1)。add操作以均摊常数时间运行,也就是说,添加n个元素需要O(n)时间。其他操作时间复杂度为O(n),以线性时间运行。与 LinkedList 实现相比,该实现的常数因子更低。
扩容:ArrayList实例的容量capacity是指存储元素的数组大小,总是大于等于列表大小。随着不断添加元素,容量会自动扩展。在添加大量元素之前,ArrayList采用ensureCapcity方法来增加实例容量,有效的减少递增式再分配的数量。
线程安全性:该实现不是同步的。多个线程同时访问一个ArrayList实例,并且存在至少一个线程对列表做结构性修改时,必须进行同步。针对于ArrayList,结构性修改是指任何添加或删除一个或者多个元素,或者显式调整底层数组大小的操作;修改元素值不是结构性修改。一般通过对自然封装该列表的对象进行同步操作来完成,如果不存在这样的对象,应该采用Collections.synchronizedList将当前列表封装起来(最好是在创建时完成,以防意外对列表的不同步访问)。
ArrayList的迭代器操作具有快速失败(Fast-Fail)机制,在创建迭代器后,除非是自身remove或add操作,任何其他对列表的结构性修改,迭代器都会抛出ConcurrentModificationException。
属性与常量
ArrayList架构比较简单,底层就是一个Object数组–elementData。
- DEFAULT_CAPACITY 表示数组初始容量,默认为10。
- size表示列表元素个数,没有volitile修饰,线程不安全。
- modCount是AbstractList抽象类中的字段,用于记录当前数组结构性修改次数。
【注】elementData是transient修饰的,不被序列化。
其他常量
// 可分配的最大数组大小,由于有些虚拟机会保留位置,分配更大数组会导致OutOfMemoryError
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
// 共享的空数组实例,用于空实例。
private static final Object[] EMPTY_ELEMENTDATA = {};
// 共享的空数组实例,用于默认大小的空实例。
// 与EMPTY_ELEMENTDATA做区分,以便了解在添加第一个元素时需要扩展多少数组容量。
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
类初始化
无参构造方法 – 默认容量
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
初始化为共享空数组DEFAULTCAPACITY_EMPTY_ELEMENTDATA,添加元素时进行扩容。
指定容量作为参数的构造方法
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
大于0,创建指定大小的Object数组;等于0,初始化共享空数组EMPTY_ELEMENTDATA;其他抛出参数不合法异常。
指定集合作为参数的构造方法
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// 如果c.toArray返回的不是Object[]类型,转为Object
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// 集合元素个数为0,使用空数组代替
this.elementData = EMPTY_ELEMENTDATA;
}
}
需要特别注意的是,ArrayList无参构造初始化时,默认创建空数组,在首次添加元素时才会初始为10。
新增与扩容实现
ArrayList类add(E e)方法,将指定元素添加到列表末尾,时间复杂度为O(1),但考虑到动态数组扩容的性质,添加操作为均摊O(1)。
public boolean add(E e) {
// 判断底层数组容量是否足够,不够则进行扩容
ensureCapacityInternal(size + 1); // Increments modCount!!
// 直接赋值--线程不安全
elementData[size++] = e;
return true;
}
在源码中,添加元素操作分为了两步:扩容和赋值。
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
// 如果底层数组仍为默认空数组,设定为初始值10
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
private void ensureExplicitCapacity(int minCapacity) {
// 列表结构性修改次数加1
modCount++;
// 如果期望最小容量大于当前数组容量,则扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
// 扩容操作
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
// 扩容后容量是当前数组容量的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 如果扩容后容量仍然小于期望最小容量,设为期望容量
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 如果扩容后容量大于JVM所能分配数组容量的最大值Integer.MAX_VALUE - 8,则使用Integer的最大值Integer.MAX_VALUE
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 创建新数组,复制原数组所有元素到新数组后,最后指针指向新数组
elementData = Arrays.copyOf(elementData, newCapacity);
}
在新增过程中,
- 没有对值做校验,因此ArrayList是允许空值的。
- 数组操作,需要特别注意边界值,例如数组下界不能小于0,上界不能大于Integer.MAX_VALUE。
- 扩容结束的赋值语句elementData[size++] = e;采用的是直接赋值,没有锁,此处线程不安全。
- 数组拷贝操作,内部调用System#arraycopy方法,是一个native方法。
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
源码问题:计算最小期望容量方法中,当elementData为DEFAULTCAPACITY_EMPTY_ELEMENTDATA时,应该直接返回默认值,而不需要Math.max判断。因为只有ArrayList无参构造初始化时,elementData才是DEFAULTCAPACITY_EMPTY_ELEMENTDATA。
private static int calculateCapacity(Object[] elementData, int minCapacity) {
// 如果底层数组仍为默认空数组,设定为初始值10
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
删除操作
ArrayList删除元素,可以通过底层数组索引删除,通过值来删除以及其他批量删除。
通过底层数组索引删除
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);
// 置空末尾元素,便于GC回收内存
elementData[--size] = null;
return oldValue;
}
通过元素值来删除
public boolean remove(Object o) {
// 值为null
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;
}
根据值删除,存在值为空和非空两种情况,在遍历时分别通过==和equals方法来匹配。需要特别注意的是,删除的是匹配到的第一个元素,而不是所有匹配元素。
private void fastRemove(int index) {
modCount++;
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
}
除了边界校验,fastRemove方法与remove(int index)方法代码几乎一致。同时我们也发现了System.arraycopy的使用,这一操作在涉及到数组操作中有着广泛应用,例如String。
其他–查询和更新元素
获取索引位置元素
public E get(int index) {
// 边界校验,index大于等于size,抛出IndexOutOfBoundsException
rangeCheck(index);
return elementData(index);
}
E elementData(int index) {
return (E) elementData[index];
}
elementData数组中元素是Object类型,获取指定类型需要强转。
更新指定索引位置元素
public E set(int index, E element) {
// 边界校验
rangeCheck(index);
E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}
ArrayList迭代器
ArrayList中提供了多种迭代器实现,例如Itr迭代器、ListIterator迭代器。Itr迭代器是AbstractList.Itr的优化版本,而ListIterator迭代器继承Itr,并对其提供了一些扩展,例如add和set操作。
Itr迭代器
private class Itr implements Iterator<E> {
// 下一个元素的索引位置
int cursor;
// 最新被返回的元素索引,如果没有更多元素,设为-1
int lastRet = -1;
// 期望结构修改次数
int expectedModCount = modCount;
Itr() {}
...
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
迭代器变量中,expectedModCount是校验数组结构修改的关键变量。而lastRet变量用于标识是否还有元素可以删除,迭代开始值为-1,无元素;remove操作后,lastRet值也置为-1,表示不能连续删除。
Itr迭代器实现Iterator接口,主要包含了3个方法。
hasNext操作
// 是否还有未迭代的元素
public boolean hasNext() {
// 如果下一个元素位置是size,表示没有元素可以迭代
return cursor != size;
}
next操作
// 返回下一个元素值
public E next() {
// 校验迭代过程中,数组是否有结构性修改
checkForComodification();
// 当前元素索引位置
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
// 迭代索引更新
cursor = i + 1;
// 返回元素值,并设置lastRet
return (E) elementData[lastRet = i];
}
remove操作
public void remove() {
// 如果数组中没有元素
if (lastRet < 0)
throw new IllegalStateException();
// 校验迭代过程中,数组是否有结构性修改
checkForComodification();
try {
// 调外部类remove,删除当前迭代元素
ArrayList.this.remove(lastRet);
// 因为是删除操作,迭代索引位置不变
cursor = lastRet;
// -1表示元素已经被删除
lastRet = -1;
// 修改期望modCount值
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
我们可以关注到:
- 迭代器的删除操作调用了外部类ArrayList的remove(int index)方法,方法必然会导致数组结构性修改(modCount++)。迭代器的remove操作会对expectedModCount进行重新赋值,这也就是自身迭代器add和remove操作不会抛ConcurrentModificationException的原因。
- remove操作中,将lastRet置为1,避免重复删除。
- remove操作通常与next方法组合使用,不单独使用。
Fail-fast机制
迭代器有expectedModCount属性用于保存迭代器被调用时的modCount值。如果在迭代过程中,迭代器的增add、删remove、改set、查next/previous 4种操作方法会对modCount进行校验是否被修改。如果被修改,抛出ConcurrentModificationException。
所谓的Fail-Fast机制核心就是下面两句
int expectedModCount = modCount;
条件表达式判断
modCount == expectedModCount
modCount属性继承自AbstractList,标识当前List结构修改的次数。这是一种fail-fast行为,用于防止并发导致不一致的结果。
protected transient int modCount = 0;
【注】注意的是modCount属性并不是volatile的。查阅ConcurrentModificationException源码注释,我们可以发现该异常并不总是表示对象被不同线程并发修改。同一个线程也可能触发该异常。例如,假如集合具有fail-fast迭代器,如果在进行迭代操作时修改数据,迭代器会抛出该异常。
* Note that this exception does not always indicate that an object has
* been concurrently modified by a <i>different</i> thread. If a single
* thread issues a sequence of method invocations that violates the
* contract of an object, the object may throw this exception. For
* example, if a thread modifies a collection directly while it is
* iterating over the collection with a fail-fast iterator, the iterator
* will throw this exception.