Collections(二)List上篇-ArrayList
List是一个有序的序列结构,基于 位置访问 ,List接口在Collection接口上新增加了一些方法,允许在指定位置插入和删除元素,也允许对元素的搜索(indexOf)。List是如何实现Iterator迭代器,ArrayList 和 LinkedList的性能差异为何会表现在不同方面,本文将对ArrayList进行详细分析。
List接口
我们先来看看List接口,提供了基于位置访问的一些方法。
| 方法 | 作用 |
|---|---|
| E get(int index) | 从某个位置获取元素 |
| E set(int index, E element) | 某个位置设置元素 |
| void add(int index, E element) | 某个位置增加元素 |
| E remove(int index) | 移除某个位置的元素 |
| int indexOf(Object o) | 搜索某个元素首次出现的位置 |
| int lastIndexOf(Object o) | 搜索某个元素最后出现的位置 |
ArrayList实现原理

ArrayList是基于可变数组实现的,内部维护了一个Object[] elementData数组,同时提供了操纵数组容量的方法,List的大小是由size变量决定的,容量是由数组的大小决定的,容量Capacity不满足size大小时,ArrayList就会自动通过合适的扩容策略进行扩容。
初始容量
ArrayList遵循上一节转换构造器的原则:提供了一个无参构造方法和Collection为参数的构造方法。
同时,还提供了一个int类型的参数来定义内部数组的容量public ArrayList(int initialCapacity),这个参数的初始容量默认是定义的一个常量:
/**
* Default initial capacity.
*/
private static final int DEFAULT_CAPACITY = 10;
两个问题:1.ArrayList什么时候会去增长容量?2.它的容量增长策略是怎么实现的呢?
需要扩容的场景肯定是因为容量无法满足元素的个数了,所以在增加元素add的时候,或者增加集合元素addAll,或者以另一个集合初始化ArrayList的时候,都会去判断是否增加容量,本章以及接下来的所有章节增长容量策略的分析都将依据 增加元素 源码进行分析。
增加元素和增长容量策略分析
直接看源码,数组的复制使用了高效的System.arraycopy方法,参数可以是不同数组,也可以作用于同一个数组。
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
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++;
}
方法ensureCapacityInternal(size + 1);是为了容量增长策略设计的,我们看下这个方法:
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++; // 这是为fail-fast机制设计的变量
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
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);
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
这部分源码其实很好理解,我们能得出以下几个结论,这些结论很好的回答了上文中提到的两个问题:
if (minCapacity - elementData.length > 0) grow(minCapacity);当元素个数size超过了内部维护的数组容量大小时,就会去扩容int newCapacity = oldCapacity + (oldCapacity >> 1);增加的容量是当前容量的二分之一,至于为什么是1/2,这应该是JDK设计者的一个折中选择问题- 采用
Arrays.copyOf复制元素到新的空间,底层也是采用System.arraycopy方法
基础源码分析
获取元素
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
rangeCheck检测角标index是否越界合法。
更新元素
public E set(int index, E element) {
rangeCheck(index);
E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}
通过set方法设置元素,会返回老的元素值。
删除元素
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;
}
和增加元素一样,删除元素的代码也采用了System.arraycopy方法进行数组移位可以得出结论,ArrayList在插入和删除操作会花费一定的代价,随机访问操作的时间复杂度将是常量级的。
索引元素
public int indexOf(Object o) {
if (o == null) {
for (int i = 0; i < size; i++)
if (elementData[i]==null)
return i;
} else {
for (int i = 0; i < size; i++)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
iterator源码实现和fail-fast设计
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
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;
return (E) elementData[lastRet = i];
}
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} 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();
}
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();
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
Itr是ArrayList的一个内部类,所以可以通过ArrayList.this方式引用ArrayList内部域。
hasNext()和next()方法的实现就是不断向后移动下标cursor,直到cursor==size为止。
上一篇文章说过,ConcurrentModificationException是由迭代器抛出来的异常,从源码中我们可以看到,当modCount != expectedModCount时,就会提前抛出异常,即fail-fast,其中expectedModCount是在构造Itr对象时产生的modCount是存在ArrayList的一个变量,在每次新增元素、删除元素或者结构改变(比如排序等),就会累加modCount,上文ensureExplicitCapacity源码中就能看到modCount++操作。
我们已经知道,在迭代集合时,是不允许更改集合内部结构的,如果希望某个符合条件的元素删除呢?答案是使用java.util.Iterator.remove()方法。
ArrayList提供了方法ListIterator<E> listIterator()获取ListIterator迭代器,这是一个为list打造的迭代器,允许双向遍历和更多操作。
public interface ListIterator<E> extends Iterator<E> {
// Query Operations
boolean hasNext();
E next();
boolean hasPrevious();
E previous();
int nextIndex();
int previousIndex();
// Modification Operations
void remove();
void set(E e);
void add(E e);
}
性能
性能问题分为时间复杂度和空间复杂度两方面去讨论。
ArrayList实现了RandomAccess接口,它长于随机访问,不擅于插入和删除,这与LinkedList是相反的。
如果初始容量过大实际元素过少,则会造成空间的浪费。如果一次插入的元素很多,而初始容量不够,则会频繁的扩容,这无疑增加的时间复杂度。
为了性能考虑,在插入大量数据的时候,我们可以选择一个合适的初始容量,如果在一个初始容量已定的ArrayList中插入大量数据,可以通过调用方法`public void ensureCapacity(int minCapacity) 确保一次性扩容足够大的容量。
如果ArrayList中元素已经添加完毕,我们可以通过调用方法 public void trimToSize() 将内部数组的容量调整为实际元素的size大小来节约空间。
下篇文章,我们将会对LinkedList进行分析。