ArrayList是一个可动态扩展的列表,底层实现是数组。首先看一下该类的继关系,如下图:
从图中可以看到ArrayList类继承自AbstractList、而AbstractList类又继承自AbstractCollection;另外实现的接口有Collection、List、Cloneable、Serialzable、Iterable和RandomAccess。
这篇博客主要分析一下其主要方法的实现。
ArrayList内有几个变量,如下:
private static final int DEFAULT_CAPACITY = 10;
private static final Object[] EMPTY_ELEMENTDATA = {};
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
transient Object[] elementData; // non-private to simplify nested class access
private int size;
上面代码中elementData就是底层的数组对象,size表示当前列表的大小。
ArrayList有三个构造方法,一个无参的,一个参数为初始容量,一个参数为集合的。分别如下:
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;
}
public ArrayList(Collection extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}
从上面可以看到,构造方法主要是完成elementData数组对象的初始化。
ArrayList的容量参数指的是elementData数组的尺寸,这个参数总是大于等于size的,一旦小于size了,就会进行扩容。
ArrayList的添加主要分为两类:添加一个元素还是添加一组元素,即一个集合。下面先看添加一个元素的add方法,再看添加一组元素的addAll方法。
add方法有两个重载,分别是add(E e)用于在列表后面添加元素,add(int index,E e)用于在指定位置添加元素。下面先看add(E e)方法的实现。
//将指定元素添加到列表末尾
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
其中ensureCapacityInternal方法用于确保elementData的容量可以放入新加的一个元素,如果放不下,那么就需要对elementData进行扩容了,不然就会报索引溢出的异常了。在容量可以保证添加一个元素后,直接将数组中最后一个位置存入元素。
下面看一个ensureCapacityInternal方法的实现:
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
//数组越界,需要进行容量扩展
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;
}
从上面可以看到,当最小容量小于10时,那么会一下子开辟容量为10的数组;如果最小容量大于10,那么就会使用最小容量。如果最小容量都比目前数组的长度大,那么就需要进行扩容了。
扩容中,首先会对现有的容量进行0.5倍的增加,如果不能满足最小容量,那么新容量就是最小容量;接下来就是对新容量与最大容量的比较,最后调用Arrays.copyOf方法将数组扩容到新容量。
至此,完成在尾部添加一个元素的过程。接下来再看add(int index,E e)在指定位置添加数据的过程。
//将元素插入到指定位置,其右边的数据右移1位
public void add(int index, E element) {
//检查索引,不能小于0,不能大于size
rangeCheckForAdd(index);
//确保容量
ensureCapacityInternal(size + 1); // Increments modCount!!
//将index右边的数据右移
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
//插入数据
elementData[index] = element;
//增加size
size++;
}
在指定位置添加元素的方法中分为5步:
1. 检查索引,不能小于0,不能大于size
2. 将容量+1,确保容量
3. 将index右边的数据右移
4. 在index位置插入数据
5. 增加size
在有了add(E e)的基础上分析add(int index,E e)就容易多了。接下来分析如何添加一组元素。
addAll方法与add类似,也有将数据添加到尾部与添加到指定位置两个重载方法。下面先看将一组数据添加到尾部的实现,代码如下:
public boolean addAll(Collection extends E> c) {
//得到待添加数组及添加的长度
Object[] a = c.toArray();
int numNew = a.length;
//确保新的容量
ensureCapacityInternal(size + numNew); // Increments modCount
//将待添加数据整个加入到elementData的尾部
System.arraycopy(a, 0, elementData, size, numNew);
//添加size
size += numNew;
return numNew != 0;
}
从上面的代码可以看到,与add方法类似,只不过这次是一下子添加一组元素。下面再看在指定位置添加一组元素的实现,代码如下:
public boolean addAll(int index, Collection extends E> c) {
//检查索引
rangeCheckForAdd(index);
Object[] a = c.toArray();
int numNew = a.length;
//确保容量
ensureCapacityInternal(size + numNew); // Increments modCount
int numMoved = size - index;
//如果有数据需要右移
if (numMoved > 0)
System.arraycopy(elementData, index, elementData, index + numNew,
numMoved);
//将一组数据从index开始添加
System.arraycopy(a, 0, elementData, index, numNew);
size += numNew;
return numNew != 0;
}
从上面代码可以看到与将一组元素添加到列表尾部的区别在于检查索引范围和将部分数据右移。
至此,添加操作就完了。主要需要记住的就是ArrayList是如何扩展容量的,默认扩展0.5倍,如果不够,那么就使用最小容量。
所谓检索,就是根据索引位置获取特定元素以及根据特定元素得到索引。
ArrayList中的get方法用于根据索引得到对象,实现如下:
public E get(int index) {
//检查索引范围
rangeCheck(index);
//返回指定位置元素
return elementData(index);
}
E elementData(int index) {
return (E) elementData[index];
}
从上面的代码可以看到get方法分为两步:
1. 检查索引范围,不能小于0,不能大于size
2. 返回指定位置元素,因为底层结构是数组,直接返回数组指定位置的元素即可
ArrayList中的indexOf方法用于根据对象从前到尾得到第一个符合的索引,如果没有该对象,那么返回-1,否则返回索引,实现如下:
public int indexOf(Object o) {
//如果对象为null
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;
}
从上面的代码可以看到,indexOf将返回对象的第一个符合的索引。而如果想得到最后一个符合的索引,那么需要调用lastIndexOf方法,实现如下:
public int lastIndexOf(Object o) {
if (o == null) {
//从后向前遍历
for (int i = size-1; i >= 0; i--)
if (elementData[i]==null)
return i;
} else {
for (int i = size-1; i >= 0; i--)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
可以看到lastIndexOf方法与indexOf方法类似,只不过是从后向前遍历,一旦找到符合的对象,直接返回索引位置。
删除操作有多种形式,有按照指定索引位置删除的,有删除具体的对象的,还有删除集合中包含的对象。
不过总体的一条原则是删除后的列表仍然得是有序的,所以将元素删除后,那么需要将元素右侧的元素左移确保列表的有序性。至于几种删除操作,下面一一讨论。
remove(int index)用于删除指定位置的元素,并返回被删除的元素,实现如下:
public E remove(int index) {
//检查索引,不能小于0,不能大于size
rangeCheck(index);
modCount++;
//得到旧值
E oldValue = elementData(index);
int numMoved = size - index - 1;
//如果有需要左移的数据,将数据左移1位
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
从remove方法中可以看到分为3步:
1. 索引范围检查
2. 得到旧值
3. 将需要左移的数据左移1位
除了可以删除指定位置的元素外,还可以删除指定对象,如下:
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;
}
从上面的代码可以看到,删除操作会根据删除的对象是否为null进行区分,因为是null就不能调用equals方法了,不然就空指针异常了。另外可以看到,remove(Object o)只会删除第一个符合要求的对象,哪怕列表中有多个相同的对象。一旦找到符合要求的对象,就调用fastRemove()方法,fastRemove()和remove(int 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
}
除了可以删除一个对象外,还可以删除一个集合中有的对象,实现如下:
public boolean removeAll(Collection> c) {
Objects.requireNonNull(c);
return batchRemove(c, false);
}
private boolean batchRemove(Collection> c, boolean complement) {
final Object[] elementData = this.elementData;
int r = 0, w = 0;
boolean modified = false;
try {
//遍历
for (; r < size; r++)
//如果集合c中不包含该数据,那么仍然保留
if (c.contains(elementData[r]) == complement)
elementData[w++] = elementData[r];
} finally {
// Preserve behavioral compatibility with AbstractCollection,
// even if c.contains() throws.
if (r != size) {
System.arraycopy(elementData, r,
elementData, w,
size - r);
w += size - r;
}
//如果有包含的数据
if (w != size) {
//将数据置为null,帮助GC
for (int i = w; i < size; i++)
elementData[i] = null;
modCount += size - w;
size = w;
modified = true;
}
}
return modified;
}
从上面代码可以看到,remove(Collection c)用于删除列表中包含集合的数据。
清空列表调用clear方法即可,该实现如下:
public void clear() {
modCount++;
// clear to let GC do its work
for (int i = 0; i < size; i++)
elementData[i] = null;
size = 0;
}
从上面代码可以看到clear操作就是将数组的每个元素置为null并将size置为0。
ArrayList可以得到两种迭代器,iterator()方法和listIterator()方法。iterator()中返回的迭代器只能从开始往后遍历,而listIterator()中返回的迭代器不止能从后往前遍历,还能从指定位置开始遍历。在操作迭代器时,允许中途调用了ArrayList的其他改变结构的方法,那么将会抛出异常,这就是所谓的fail-fast机制。在前面的很多方法中,都出现了modCOunt变量,该变量就是用于处理fail-fast机制的。在初始化迭代器时保存当前的modCount,然后在每个操作时检查当前的modCount是否与初始化时的一致,如果不一致,说明进行了改变结构的操作,那么将会抛出异常。
下面分别分析这两个方法返回的迭代器。
iterator()方法返回的迭代器按照从前到后的顺序迭代数据。实现如下:
public Iterator iterator() {
return new Itr();
}
/**
* An optimized version of AbstractList.Itr
*/
private class Itr implements Iterator<E> {
int cursor; //下一个返回元素的索引
int lastRet = -1; // 返回的最后一个元素的索引
int expectedModCount = modCount; //初始化时保存当前modCount
//是否有下一个元素,只要下一个返回元素的索引不是size,就说明还有下一个元素
public boolean hasNext() {
return cursor != size;
}
@SuppressWarnings("unchecked")
public E next() {
//检查modCount是否改变
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;
//由于remove中改变了modCount,重新保存当前的modCount
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
//检查modCount是否改变了,如果改变了,直接抛出异常
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
Iterator的原理如下图:
上图中,初始时cursor指向0,一旦调用next()方法cursor和lastRet就会+1,就会右移,而一旦调用remove()方法就会删除lastRet所指向的元素。cursor指向下一个返回的元素,lastRet指向最后一个返回的元素,所以lastRet=cursor-1。
listIterator()方法有几个重载方法,其返回值都是一个ListIterator对象,如下:
public ListIterator listIterator(int index) {
if (index < 0 || index > size)
throw new IndexOutOfBoundsException("Index: "+index);
return new ListItr(index);
}
public ListIterator listIterator() {
return new ListItr(0);
}
无参的开始索引为0,有参的可以指定迭代器开始的位置。ListItr的实现如下:
private class ListItr extends Itr implements ListIterator<E> {
ListItr(int index) {
super();
//将下一个返回元素的位置设置为index
cursor = index;
}
//是否有前一个值
public boolean hasPrevious() {
return cursor != 0;
}
//后一个值的索引
public int nextIndex() {
return cursor;
}
//前一个值的索引
public int previousIndex() {
return cursor - 1;
}
//得到前一个值,与next()相对
@SuppressWarnings("unchecked")
public E previous() {
checkForComodification();
int i = cursor - 1;
if (i < 0)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i;
return (E) elementData[lastRet = i];
}
//更改当前lastRet的值
public void set(E e) {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.set(lastRet, e);
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
//在cursor位置添加数据
public void add(E e) {
checkForComodification();
try {
int i = cursor;
ArrayList.this.add(i, e);
cursor = i + 1;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
}
ListItr继承自Itr并实现了ListIterator接口,既可以实现向前遍历,又可以实现向后遍历,并且除了删除操作外,还可以添加数据、更改数据。
其结构如下图:
cursor游标既可以左移也可以右移,当调用previous()时就将返回cursor的前一个数据。
ArrayList的底层基于数组,并且是一个可以自动扩容的List,一切方法都是基于数组之上的,这是和LinkedList的区别;并且该类是一个线程不安全的类,这是和Vector类的区别。