前言
集合类中,最基础也是最常用的,大概就是ArrayList了吧。
ArrayList的本质,是一个可变长的数组。
那有人可能就会问,哎呀这个数组老简单了,有什么好看的啊……但事实上,在面试时,有些人还是对源码的细节说不清楚,从而留下较差的印象。
这里,我就带着大家,一点一点地梳理一下,ArrayList的底层源码吧。
概览
首先我们从全局把握一下这个类,这个类的签名如下:
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
ArrayList类继承自AbstractList类,且实现了List、RandomAccess、Cloneable和Serializable接口。其中要提一嘴的是,RandomAccess接口是个空接口,作为一个标志使用,当一个类支持随机访问的时候(数组是很典型的),就可以标记这个接口。
在这个类的JavaDoc中,描述了ArrayList的一些特征,主要如下:
允许 put null 值,会自动扩容;
size、isEmpty、get、set、add 等方法时间复杂度都是 O(1);
是非线程安全的,多线程情况下,推荐使用线程安全类:Collections#synchronizedList;
增强 for 循环,或者使用迭代器迭代过程中,如果数组大小被改变,会快速失败,抛出异常。
JavaDoc中还提到了fail-fast机制,这个会在下面将迭代器时提到。
初始化
ArrayList有三种初始化方法,其中一种是使用现有的集合来进行初始化,就不说了。主要看下面这两种:
private static final Object[] EMPTY_ELEMENTDATA = {};
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
public ArrayList() {
this.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。第二种需要指定初始化的容量,如果参数大于0,就会构造一个对应大小的Object数组,如果参数为0,就使用一个容量为0的空数组作为容器,如果参数小于0,就抛出一个异常。
有人可能就会问,哎这个无参构造和有参构造参数为0都是长度为0的空数组,为什么还用两个不同的数组呢?这是因为,在首个元素被添加后,数组是需要进行扩容的,两种构造方法构造出的数组,扩容到的大小是不同的,使用这个来加以区分。
这里一个很重要的点,就是在无参初始化的时候,构造出的底层数组长度为0,并不是大家所说的10,在添加第一个元素时数组长度才会扩展到10。
添加元素和扩容
ArrayList添加元素有两种方法,如下:
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()方法来确保数组容量可用,如果不够用就会在该方法内进行扩容,接着在elementData[size]位置放置e,并且siez自加1。
第二种方法则是在要求的位置放置某个元素,首先会调用rangeCheckForAdd()方法来检查index位置是否可用,其实也只是判断下是否超出数组范围,超出的话就会抛出IndexOutOfBoundsException异常。接着使用ensureCapacityInternal()方法检查数组容量,使用System.arraycopy方法将index位置及其之后的元素向后拷贝一个单位,再将待插入元素放置在index处即可。
这里放置新元素的时候没有进行任何的判断,所以ArrayList是允许null值的,且放置是没有加锁,使得ArrayList是线程不安全的。
显然,扩容部分的核心实现就在ensureCapacityInternal()方法中,我们来看看这个方法。
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
ensureCapacityInternal方法调用了ensureExplicitCapacity方法,该方法首先将modCount自增一,接着判断是否需要扩容,即判断需要的容量是否大于现有的容量,如果大于则调用grow()方法将底层数组扩容到minCapacity,否则无动作。
注意下ensureExplicitCapacity()方法传入的值是经过calculateCapacity()方法计算后的值,该方法实现如下:
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
这个方法就是上面说的:为什么都是空数组却用不同的变量存储的原因,我们注意到,如果elementData和DEFAULTCAPACITY_EMPTY_ELEMENTDATA是同一个数组,即该ArrayList是使用无参构造构建的,那么我们就需要返回DEFAULT_CAPACITY和minCapacity中较大的值。那么在第一次插入的时候,显然DEFAULT_CAPACITY较大,默认值为10,那么我们第一次插入,数组就会扩容为10。而使用有参构造参数为0的方法的话,在这一步返回的仅仅是1。
grow()方法的实现很简洁,如下:
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);
}
除去最后一行,前面都是对newCapacity进行赋值,并且判断是否溢出之类的。
需要注意的是,newCapacity的选取,如果不考虑扩容溢出等情况,newCapacity的值为:oldCapacity + (oldCapacity >> 1),即旧容量的1.5倍。最后一行就是将旧数组的元素拷贝到新数组即可。
该方法的最后一个判断,将新的大小和MAX_ARRAY_SIZE的值比较,如果超过了这个值,那么newCapacity就会被赋为hugeCapacity()方法的返回值。MAX_ARRAY_SIZE的值为Integer.MAX_VALUE - 8,hugeCapacity()方法传入的参数是最少需要的数组容量。在hugeCapacity()方法中有如下:
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
如果最少需要的值大于MAX_ARRAY_SIZE的话,就只会扩容到Integer.MAX_VALUE,否则就扩容到MAX_ARRAY_SIZE。这里就说明了ArrayList的容量上限为Integer.MAX_VALUE。如果达到了该值,就不会再为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);
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
核心的删除就是System.arraycopy()方法,直接将index+1及其后面的所有元素向前移动了一个单位,那么index位置的元素就被覆盖住了,这时数组的最后一个位置应当是空的,那么就直接赋为null,并且size自减1。那么原本的最后一个元素就会被GC回收。
第二种remove的方式是根据元素进行删除,如下:
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;
}
整体的逻辑是,判断传入的元素是否为null,如果是null,就选择删掉List中第一个null(ArrayList中允许有多个null),不是null的话就遍历元素,删除第一个equals的元素。注意这里使用的是equals()方法,如果不是基本类型的话,我们就需要关注equals()方法的具体实现。
删除的核心逻辑是fastRemove()方法,该方法传入要删除元素的下标:
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
}
这个方法很简单,逻辑和第一个remove()方法一致,也就是将后面的元素向前移动而已。
注意,在删除时,底层数组并没有进行索容,事实上,在扩容之后,数组的容量就永远不会小于该容量。
迭代器
要实现迭代器,只需要实现Iterator接口即可。ArrayList中实现了一个Itr类作为迭代器,并且在ArrayList中有一个iterator()方法,用于返回这个类的一个实例。
private class Itr implements Iterator
1
Iterator接口一般来说需要实现三个方法:
hasNext(),是否还有值未迭代
next(),迭代下一个值
remove(),删除正在迭代的值
讲解方法之前,先看一些重要的属性。
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
cursor表示迭代过程中的下一个元素的下标;lastRet表示上一次迭代过程的索引位置;expectedModCount表示期望的修改次数(版本号),这个值的初始值设置为数组的修改次数(版本号)。
hasNext()
public boolean hasNext() {
return cursor != size;
}
该方法的实现非常简单,当前的cursor总是指向下一个元素,若cursor等于size,说明当前迭代的元素为最后一个元素,hasNext()返回false即可,否则一直有值未迭代,返回true。
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;
return (E) elementData[lastRet = i];
}
该方法返回下一个迭代的元素,该方法开头首先调用checkForComodification()方法以确保在迭代过程中ArrayList没有被修改,该方法实现如下:
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
这里仅仅是将modCount与expectedModCount相比较,两个值不同的情况下就会抛出ConcurrentModificationException异常。
修改数组的一些操作,例如add()或者remove()方法,都会使数组的modCount改变,如果在迭代过程中进行了这些操作,那么下一次next()方法就会抛出异常,这被称为fail-fast机制。
我们继续看next()方法。注意下面的操作:
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
首先判断i与size的大小,如果i大于等于size,说明越界了,需要抛出异常。接着又将该List的elementData的引用赋给了局部变量elementData,又判断了一次i与这个elementData的length大小。
这时有人就会问了,唉这不是多此一举吗,两次的i都是和同一个数组的大小相比较,返回的结果肯定一样啊。这可就不一定了,我们需要考虑的是,多线程的情况。有可能,在判断第一个的时候,没有发生越界,但是此时该线程被打断,另一个线程删掉了一些数据,造成了减小,那么这个线程在进行第二次判断的时候,就可能造成越界,注意这里抛出的异常是ConcurrentModificationException,说明其主要问题不在于越界,而在于迭代期间修改。
remove()
那么我们如何在迭代期间安全地删除数值呢,此时就可以使用Iterator的remove()方法:
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();
}
}
这里的删除很常规,主要还是调用ArrayList的remove()方法,但是在删除完成后,重新对expectedModCount进行了赋值,使得在下一次检查时保证相等。