本篇分析ArrayList的源码,在分析之前先跟大家谈一谈数组。数组可能是我们最早接触到的数据结构之一,它是在内存中划分出一块连续的地址空间用来进行元素的存储,由于它直接操作内存,所以数组的性能要比集合类更好一些,这是使用数组的一大优势。但是我们知道数组存在致命的缺陷,就是在初始化时必须指定数组大小,并且在后续操作中不能再更改数组的大小。在实际情况中我们遇到更多的是一开始并不知道要存放多少元素,而是希望容器能够自动的扩展它自身的容量以便能够存放更多的元素。ArrayList就能够很好的满足这样的需求,它能够自动扩展大小以适应存储元素的不断增加。它的底层是基于数组实现的,因此它具有数组的一些特点,例如查找修改快而插入删除慢。本篇我们将深入源码看看它是怎样对数组进行封装的。
ArrayList的继承体系结构
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
ArrayList类主要继承自AbstractList并实现了List接口,实现了Cloneable、Serializable接口以便进行克隆和序列化操作。
ArrayList的主要属性
// 默认初始化容量
private static final int DEFAULT_CAPACITY = 10;
// 空对象数组
private static final Object[] EMPTY_ELEMENTDATA = {};
// 用于默认大小的空实例的共享空数组实例
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// elementData用来存储ArrayList中的元素,由此可以看出,ArrayList底层是借组于数组来实现的。
transient Object[] elementData; // non-private to simplify nested class access
// 记录当前集合元素个数
private int size;
// 集合的最大容量
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
ArrayList的构造方法
// 指定容量作为参数的构造方法
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
// 新建指定容量的Object类型数组
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
// 将elementData指向空的数组实例
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
// 无参构造,初始化容量为默认容量即(10)
public ArrayList() {
// 将elementData指向空的数组实例
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
// 传入外部集合的构造方法
public ArrayList(Collection<? extends E> c) {
// 将elementData指向传入集合的内部数组
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// 判断引用的数组类型, 并将引用转换成Object数组引用
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// 将elementData指向空的数组实例
this.elementData = EMPTY_ELEMENTDATA;
}
}
上面这部分,相信大家一定都不陌生,接下来我们将会继续介绍ArrayList的其他常用方法。
ArrayList的常用方法
get(int index)
public E get(int index) {
Objects.checkIndex(index, size);
// 直接利用底层数组的随机存取特性返回指定下标的元素,时间复杂度为O(1),效率很高,适合查询
return elementData(index);
}
set(int index, E element)
public E set(int index, E element) {
Objects.checkIndex(index, size);
E oldValue = elementData(index);
// 利用数组随机存取的特性,直接定位到指定下标的元素,并替换成新的元素,时间复杂度为O(1),效率高,适合修改
elementData[index] = element;
// 返回原来位置的元素值
return oldValue;
}
add(E e)
// 向集合末尾添加元素e
public boolean add(E e) {
modCount++;
add(e, elementData, size);
return true;
}
由add(E e)方法的源码可以看出,此处调用了 private void add(E e, Object[] elementData, int s) 方法:
private void add(E e, Object[] elementData, int s) {
// 当前数组容量已满,则调用grow()方法扩容
if (s == elementData.length)
elementData = grow();
elementData[s] = e;
size = s + 1;
}
为了解决困惑,我们再看看 grow() 方法是如何进行扩容的:
private Object[] grow() {
return grow(size + 1);
}
private Object[] grow(int minCapacity) {
// 开辟一个新的长度的数组,并将elementData指向复制了elementData中元素复的新数组
return elementData = Arrays.copyOf(elementData, newCapacity(minCapacity));
}
原来在调用grow()方法的时候,会转而调用grow(size + 1)。我们发现在调用 grow() 方法的时候,又调用了 private int newCapacity(int minCapacity) 方法,于是我们接着看它的源码:
private int newCapacity(int minCapacity) {
// 集合之前的容量
int oldCapacity = elementData.length;
// 扩容后的容量,可以看出为之前的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 扩容1.5倍后仍不能到达最小需求容量
if (newCapacity - minCapacity <= 0) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
return Math.max(DEFAULT_CAPACITY, minCapacity);
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return minCapacity;
}
// 扩容后的容量超过MAX_ARRAY_SIZE时,返回hugeCapacity(minCapacity)方法的返回值
return (newCapacity - MAX_ARRAY_SIZE <= 0)
? newCapacity
: hugeCapacity(minCapacity);
}
此处调用了 private static int hugeCapacity(int minCapacity) 方法,我们接着追溯下去:
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE)
? Integer.MAX_VALUE
: MAX_ARRAY_SIZE;
}
至此终于结束了这么多层的嵌套调用,add(E e)方法的庐山真面目便被揭晓。从源码中可以看到,add(E e)方法的功能在集合末尾增加一个元素,当集合底层的数组容量用满时,将会进行数组容量的扩充,即重新开辟一个更长的数组,并将之前数组中的元素copy到新数组中,新开辟的数组一般情况下是扩展到原来数组长度的1.5倍,如果扩张1.5倍太小,就用我们需要的空间大小minCapacity来进行扩张。如果扩张1.5倍太大或者是我们所需的空间大小minCapacity太大,则进行Integer.MAX_VALUE来进行扩张。
add(int index, E element)
public void add(int index, E element) {
rangeCheckForAdd(index);
modCount++;
// 用来保存之前的集合元素个数
final int s;
Object[] elementData;
// 容量已满,需要扩容
if ((s = size) == (elementData = this.elementData).length)
elementData = grow();
System.arraycopy(elementData, index,
elementData, index + 1,
s - index);
elementData[index] = element;
size = s + 1;
}
remove(int index)
// 删除指定位置的元素
public E remove(int index) {
Objects.checkIndex(index, size);
final Object[] es = elementData;
@SuppressWarnings("unchecked") E oldValue = (E) es[index];
fastRemove(es, index);
return oldValue;
}
此处主要是通过调用 fastRemove(Object[] es, int i) 方法实现指定位置元素的删除操作的,于是我们来看这个方法的源码:
private void fastRemove(Object[] es, int i) {
modCount++;
final int newSize;
if ((newSize = size - 1) > i)
// 将要删除位置的元素后面的元素一次向前移动一位
System.arraycopy(es, i + 1, es, i, newSize - i);
// 将移动后的第(元素个数 - 1)位置位null
es[size = newSize] = null;
}
显然在 remove(int index) 方法中,移动了大量的元素,导致整个删除过程的时间复杂度为O(n),效率太低,不适合做删除操作。
remove(Object o)
// 删除值为o的第一个元素
public boolean remove(Object o) {
final Object[] es = elementData;
final int size = this.size;
int i = 0;
// 先查找出第一个值为o的元素的位置
found: {
if (o == null) {
for (; i < size; i++)
if (es[i] == null)
break found;
} else {
for (; i < size; i++)
if (o.equals(es[i]))
break found;
}
return false;
}
// 再根据查找出的元素的位置删除将该元素从集合中删除
fastRemove(es, i);
return true;
}
remove(Object o) 方法中,先是在集合底层的数组中查询到第一个值为要删除的元素值的元素的下标,然后调用 fastRemove(es, i) 方法进行指定位置元素的删除,查询和删除的时间复杂度都是O(1),显然效率也是很低的。
clear()
最后要介绍的函数就是这个了,清除ArrayList中所有的元素。
直接将数组中的所有元素设置为null即可,这样便于垃圾回收。
public void clear() {
modCount++;
final Object[] es = elementData;
for (int to = size, i = size = 0; i < to; i++)
es[i] = null;
}
至此,我们对ArrayList中比较常用的方法做了简单的分析:
- ArrayList底层实现是基于数组的,因此对于指定位置查找修改等操作效率很高,但是对于删除和插入操作比较效率就比较低了。
- 构造ArrayList时尽量指定容量,减少扩容时带来的数组复制操作。
- ArrayList的所有方法都没有进行同步,因此它不是线程安全的。