ArrayList 源码
ArrayList概览
基本概念
ArrayList 的结构较为简单,就是一个数组。结构如下图所示。
ArrayList中有一些重要概念、属性:
- index:当前下标
- elementData:数组,该数组的大小,经常与 ArrayList 的size 混淆,需要注意。
- DEFAULT_CAPACITY:数组的初始大小,默认是 10
- size 表示当前ArrayList实际有多少个数据,没有使用 volatile 修饰;
- modCount :当前数组的版本号,数组结构有变动,就会 +1。
类介绍(注释)
类注释大致内容如下:
- 允许 null 值,会自动扩容,实现了List接口的所有方法;
- size、isEmpty、get、set、add 等方法时间复杂度都是 O (1);
- 是非线程安全的,多线程情况下,推荐使用线程安全类:Collections#synchronizedList;
- 增强 for 循环,或者使用迭代器迭代过程中,如果数组大小被改变,会快速失败,抛出ConcurrentModificationException异常。
源码解析
构造方法
ArrayList提供了三种构造方法。
- 无参构造方法
- 指定大小
- 指定初始数据
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//无参数直接初始化,数组大小为空
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
// 指定大小,主要是判断指定大小的合理性(>=0)
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(Collection<? extends E> c) {
elementData = c.toArray();
// c 有内容
if ((size = elementData.length) != 0) {
// 如果集合元素类型不是 Object 类型,我们会转成 Object
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// c 没有内容,则为空数组
this.elementData = EMPTY_ELEMENTDATA;
}
}
构造方法补充
- ArrayList 无参构造器初始化时,默认大小是空数组,并不是大家常说的 10,10 是在第一次 add 的时候扩容的数组值。
- 指定初始数据初始化时,我们发现一个这样子的注释 c.toArray might (incorrectly) not return Object[] see 6260652,这是 Java8 的一个 bug,意思是当给定集合内的元素不是 Object 类型时,我们会转化成 Object 的类型。一般情况下都不会触发此 bug,只有在下列场景下才会触发:ArrayList 初始化之后(ArrayList 元素非 Object 类型),再次调用 toArray 方法,得到 Object 数组,并且往 Object 数组赋值时,才会触发此 bug。官方查看文档地址:https://bugs.java.com/bugdatabase/view_bug.do?bug_id=6260652 ,问题在 Java 9 中被解决。
添加元素、扩容
ArrayList 提供了两种方式添加元素。我们选取较简单的一种,即在末尾添加。
添加元素分为两步:
- 确认容量是否足够,不足则扩容
- 在末尾赋值
public boolean add(E e) {
// 判断容量是否足够(当前大小+1)
ensureCapacityInternal(size + 1); // Increments modCount!!
// 直接赋值,非线程安全操作
elementData[size++] = e;
return true;
}
接下来追溯到ensureCapacityInternal方法的源码,为了便于理解,以下是简单整理所得,并非原版。
private void ensureCapacityInternal(int minCapacity) {
// 如果初始化数组大小时,没有给定初始值,才会走if分支
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity();
}
private void ensureExplicitCapacity(int minCapacity) {
// 版本号+1
modCount++;
// 如果我们期望的容量,(即add操作时,传入的size+1) 超过 目前数组的长度,那么就扩容
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
最后是扩容-grow
方法源码
// 允许JVM分配的最大数组空间
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
// oldCapacity >> 1 相当于 oldCapacity / 2
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 如果扩容后的容量 < 我们的期望值
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 如果扩容后的容量 > 允许JVM分配的最大数组空间,则使用 Integer.MAX_VALUE
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
// 通过Arrays.copyOf方法进行扩容,同时复制了数据
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;
}
在添加元素、扩容的源码中,我们应该注意:
- 扩容是原来容量大小 + 容量大小的一半,直白来说,扩容后的大小是原来容量的 1.5 倍;
- ArrayList 中的数组的最大值是 Integer.MAX_VALUE,超过这个值,在hugeCapacity方法中,也只会给Integer.MAX_VALUE。
- 新增时,并没有对值进行严格的校验,所以 ArrayList 是允许 null 值的。
- 源码在扩容的时候,有数组大小溢出意识,就是说扩容后数组的大小下界不能小于 0,上界不能大于 Integer 的最大值
扩容的核心实现
从以上源码,我们发现grow
方法的核心实现,在于以下一句。
elementData = Arrays.copyOf(elementData, newCapacity);
删除元素
ArrayList提供了多个重载的remove方法,其中的实现大同小异,下面以remove``(``**Object **``o``)
,即删除指定对象的方式为例。
public boolean remove(Object o) {
// 需要删除的对象为 null
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
// 删除第一个找到的null值后,直接结束方法
fastRemove(index);
return true;
}
} else {
// 需要删除的对象不为 null
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
// 通过equals方法,删除第一个找到的指定元素后,直接结束方法
fastRemove(index);
return true;
}
}
return false;
}
以上源码中,可以发现,最终都是调用了fastRemove(index);通过索引来快速删除元素,除此之外需要额外注意:
- 新增的时候是没有对 null 进行校验的,所以删除的时候也是允许删除 null 值的;
- 需要删除的对象不为 null时,找到值在数组中的索引位置,是通过 equals 来判断的,如果数组元素不是基本类型,我们需要关注 equals 的具体实现。
下面来看看fastRemove的实现:
那为什么叫快速删除呢?通过注释可以看出,此方法跳过了下标范围检测、未返回任何值。
/*
* Private remove method that skips bounds checking and does not
* return the value removed.
*/
private void fastRemove(int index) {
// 更新版本号
modCount++;
// 计算需要移动的元素个数(从中间删除后,保证数组连续,会将后方元素前移)
// 减 1 的原因,是因为 size 从 1 开始算起,index 从 0开始算起
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
}
迭代(Iterator)
ArrayList通过实现 java.util.Iterator接口,达到迭代器的效果。以下是几个重要参数:
- int cursor;// 迭代过程中,下一个元素的位置,默认从 0 开始。
- int lastRet = -1; // 新增场景:表示上一次迭代过程中,索引的位置;删除场景:为 -1。
- int expectedModCount = modCount;// expectedModCount 表示迭代过程中,期望的版本号;
迭代器的实现,通常有hasNext、next、remove 三个方法。以下为ArrayList中方法的实现:
public boolean hasNext() {
// cursor 表示下一个元素的位置,size 表示实际元素个数
// 如果两者相等,说明已经没有元素可以迭代了;如果不相等,说明还可以迭代
return cursor != size;
}
public E next() {
// 迭代过程中,判断版本号是否被修改。如被修改,抛出 ConcurrentModificationException
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];
}
// 判断版本号是否被修改。如被修改,抛出 ConcurrentModificationException
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
从源码中可以看到,next 方法就干了两件事情,第一是检验能不能继续迭代,第二是找到迭代的值,并为下一次迭代做准备( cursor = i + 1)。再看remove方法,我们需要注意其中两点:
- lastRet = -1 的操作目的,是防止重复删除操作
- 删除元素成功,数组当前 modCount 就会发生变化,这里会把 expectedModCount 重新赋值,下次迭代时两者的值就会一致了
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
// 判断版本号是否被修改
checkForComodification();
try {
// 这里调用的是 ArrayList的remove方法
ArrayList.this.remove(lastRet);
cursor = lastRet;
// -1 表示元素已经被删除,这里也防止重复删除
lastRet = -1;
// 删除元素时 modCount 的值已经发生变化,在此赋值给 expectedModCount
// 这样下次迭代时,两者的值是一致的了
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
缩容
ArrayList并未提供自动缩容的机制,但可以通过手动调用trimToSize方法,来使其空间利用率达到100%。
如以下场景:系统启动,初始化一些数据到ArrayList中缓存起来,这些数据比较多(几千个元素)但是根据业务场景是不会变的。源码较为简单:
public void trimToSize() {
// 增加版本号
modCount++;
if (size < elementData.length) {
elementData = (size == 0)
? EMPTY_ELEMENTDATA
: Arrays.copyOf(elementData, size);
}
}
ArrayList的线程安全
只有当 ArrayList 作为共享变量时,才会有线程安全问题,当 ArrayList 是方法内的局部变量时,是没有线程安全的问题的。
ArrayList 有线程安全问题的本质,是因为 ArrayList 自身的 elementData、size、modConut 在进行各种操作时,都没有加锁,而且这些变量的类型并非是可见(volatile)的,所以如果多个线程对这些变量进行操作时,可能会有值被覆盖的情况。
类的注释中推荐我们使用 Collections#synchronizedList 来保证线程安全,SynchronizedList 是通过在每个方法上面加上锁来实现,虽然实现了线程安全,但是性能大大降低,如add操作:
public void add(int index, E element) {
// synchronized 是一种轻量锁,mutex 表示一个当前 SynchronizedList
synchronized (mutex) {list.add(index, element);}
}
时间复杂度
从我们上面新增或删除方法的源码解析,根据数组索引对数组元素的操作,复杂度是 O (1)。而遇到扩容时,复杂度为O(n)。