一、概述
ArrayList是一个用来顺序存储元素的集合,它是有序且可以随机访问的。正如它的名字所示,它的底层数据结构就是用数组实现的。那为什么不直接用数组存储元素呢?因为ArrayList提供了一系列人性化的API,迭代器以及自动扩容机制,使你不必关心数组元素的迁移变动。有几个关键的成员变量需要我们重点关注一下:
- elementData:一个对象数组,ArrayList用于存储元素的关键数据结构
- size:elementData数组中已经存储的元素数量
- modCount:ArrayList的修改次数,详细分析可以移步第三小节
不过需要注意的是ArrayList不是线程安全的,如需要多线程操作可以使用CopyOnWriteArrayList,该类利用一个写时复制的机制,通过加锁的方式依次给每个线程拷贝一个数组副本,在线程修改完副本后,将主数组引用指向该副本。
还有一种方法是使用工具类Collections的synchronizedList方法,该方法用到了装饰器模式,维护了一个内部类SynchronizedList,该类所有的方法都用到了synchronized同步锁,从而改变了原list的行为。
二、常用API源码分析
1.初始化
先来看下ArrayList的成员变量和构造方法
// java.util.ArrayList
// ArrayList数组默认初始化容量为10
private static final int DEFAULT_CAPACITY = 10;
// 空元素数组
private static final Object[] EMPTY_ELEMENTDATA = {};
// 默认容量的空元素数组,默认无参构造方法会使用到
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 关键数据结构,ArrayList用来存储元素的数组
transient Object[] elementData;
// 数组中已存储元素的数量
private int size;
// 指定数组容量的构造方法
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);
}
}
// 默认容量的构造方法
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
// 利用另外一个集合构造ArrayList
public ArrayList(Collection<? extends E> c) {
// 将c转为数组,elementData指向该数组
elementData = c.toArray();
if ((size = elementData.length) != 0) {
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
this.elementData = EMPTY_ELEMENTDATA;
}
}
2.增加元素
增加元素主要有两种方式,一种是指定插入位置add(int index, E element)
,另一种是尾部插入add(E e)
。首先来看下尾插法,该方法的实质是往数组已存储元素的后一个位置插入新元素,这个逻辑并不复杂,比较值得一看的是自动扩容部分(或者数组初始化)的代码。
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);
}
// 数组已经初始化的情况下返回size+1
return minCapacity;
}
private void ensureExplicitCapacity(int minCapacity) {
// 修改计数器加一
modCount++;
// 当size+1超过数组长度时需要扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
// 在原来的基础上扩容50%
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 溢出以及int最大值的判断
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// Arrays.copyOf方法会根据传入的数组长度创出一个新数组,同时将原数组的元素复制进新数组中,元素下标不变,新数组空位是null
elementData = Arrays.copyOf(elementData, newCapacity);
}
再来看下指定位置的插入方法add(int index, E element)
,扩容机制和上面代码相同就不再多说了,主要差别体现在:指定插入位置会使该位置之后的元素后移一格
public void add(int index, E element) {
// 检查入参index是否越界或小于0
rangeCheckForAdd(index);
// 检查容量,这个方法和上面add中的相同,不再展开
ensureCapacityInternal(size + 1); // Increments modCount!!
// arraycopy方法使index后的数组元素后移一格
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
// 将新元素插入index位置
elementData[index] = element;
size++;
}
3.获取元素
获取元素的源码非常简单,就是先检查index是否合法,然后将index位置的元素返回回来。
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
E elementData(int index) {
return (E) elementData[index];
}
4.删除元素
删除元素也有两种方式,第一种是根据数组下标删除remove(int index)
,第二种是根据对象删除remove(Object o)
第一种方式的实质将index后的数组元素前移一格,并将末尾置为null
public E remove(int index) {
rangeCheck(index);
modCount++;
// 找到该索引的数组元素,用于当做返回值
E oldValue = elementData(index);
int numMoved = size - index - 1;
// index后的数组元素前移一格
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
// 由于数组元素前移了,所以末尾置为null
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
再来看下第二种删除方式remove(Object o)
和第一种方式相比多了一步根据对象找寻索引的过程。
public boolean remove(Object o) {
if (o == null) {
// 遍历数组找寻第一个null的索引
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
// 遍历数组找寻第一个对象o的索引,用equals方法判断相等
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
// index后的元素前移覆盖和第一种删除的代码相同
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
}
三、迭代器和fail-fast
ArrayList提供了两种迭代器,一种是单向迭代的Itr
还有一种是双向迭代的ListItr
后者是前者的子类。使用iterator()
方法能够构造出当前ArrayList的Itr迭代器;使用listIterator()
能够构造出ListItr迭代器。我们常用的foreach语法其实是个语法糖,把class文件反编译后会发现其实它用的就是Itr迭代器。
由于ArrayList不是线程安全的,所以迭代器都实施了一种快速失败(fail-fast)的机制,这个机制会对比ArrayList的修改次数来快速抛出异常,而不是尽最大努力去遍历元素。这个修改次数是由modCount变量来统计的,正如上面分析的源码所示,增加或者删除元素时都会将modCount加一。
private class Itr implements Iterator<E> {
int cursor; // 游标,指向当前指向的元素
int lastRet = -1; // 上一个返回的数组元素的下标
int expectedModCount = modCount; // 期望修改次数
Itr() {}
public boolean hasNext() {
return cursor != size;
}
@SuppressWarnings("unchecked")
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];
}
public void remove() {
// 没有调用过next()方法就remove或者连续remove两次
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification(); // 检查迭代中是否有线程增删过元素
try {
// 调用外部类ArrayList的remove方法删除元素,该方法会修改modCount
ArrayList.this.remove(lastRet);
cursor = lastRet; // 游标指向上一个返回的元素
lastRet = -1;
expectedModCount = modCount; // 同步modCount到expectedModCount
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
@Override
@SuppressWarnings("unchecked")
public void forEachRemaining(Consumer<? super E> consumer) {
Objects.requireNonNull(consumer);
final int size = ArrayList.this.size;
int i = cursor;
if (i >= size) {
return;
}
final Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length) {
throw new ConcurrentModificationException();
}
// 自旋的过程也要检查modCount
while (i != size && modCount == expectedModCount) {
consumer.accept((E) elementData[i++]);
}
// update once at end of iteration to reduce heap write traffic
cursor = i;
lastRet = i - 1;
checkForComodification();
}
// 当expectedModCount和不符合modCount时,就说明在迭代过程中有线程增删过元素,抛出异常
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
从上述源码中我们可以获悉:使用foreach语法时不能使用ArrayList的remove方法。想要在迭代过程中删除元素必须使用迭代器的remove方法,因为它会同步modCount。下面是一段测试代码
public void testForEach() {
List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");
for (String s : list) {
// list.remove(s); // 抛出ConcurrentModificationException异常
// list.add("test"); // 抛出ConcurrentModificationException异常
list.set(1, "22"); // ok
System.out.println(s);
}
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String s = iterator.next();
// list.remove(s); // 抛出ConcurrentModificationException异常
iterator.remove();
}
System.out.println(list);
}