ArrayList源码解析
ArrayList是常用的结构,也是面试过程中可能被问到的知识点,需要对源码的细节进行了解。
1.整体架构
ArrayList整体架构比较简单,是一个数组结构,
上图展示的是长度为10的数组(10也是ArrayList的默认最小初始化长度),ArrayList关键成员变量如下,
变量名 | 说明 |
---|---|
elementData | 数组本身 |
index | 数组索引 |
DEFAULT_CAPACITY | 数组初始大小,默认是10 |
size | 当前数组的大小,类型是int,未使用volatile修饰,所以是线程不安全的 |
modCount | 当前数组被修改的次数,也称为版本号,数组结构有变动就会+1 |
2.源码解析
初始化
初始化的方式有3种,
- 无参数初始化
- 指定大小初始化
- 指定初始数据初始化
具体源码如下,
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
/**
* Default initial capacity.
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* Shared empty array instance used for empty instances.
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
/**
* Shared empty array instance used for default sized empty instances. We
* distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
* first element is added.
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
* 无参数初始化
* Constructs an empty list with an initial capacity of ten.
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
/**
* 指定大小初始化
* Constructs an empty list with the specified initial capacity.
* @param initialCapacity 指定列表长度
* @throws IllegalArgumentException 当指定值是负数时报错
*/
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);
}
}
/**
* 指定初始数据初始化
* @param c 传入Collection对象
* @throws NullPointerException 当传入的对象为null时报错
*/
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[](这种情况比较少见)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else { // 当转换得到的数组长度为0时,replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}
}
初始化方式 | 说明 |
---|---|
无参数初始化 | 初始化一个空数组(起初并未完全初始化),当添加元素时才完全初始化,其长度为10 |
指定大小初始化 | 当指定值为负数会报错;当指定值为0,默认引用EMPTY_ELEMENTDATA空数组;当指定值大于0,创建指定大小的数组 |
指定初始数据初始化 | 调用集合对象的toArray方法得到数组,对数组长度及返回类型进行校验。如果数组长度为0,则elementData引用EMPTY_ELEMENTDATA空数组。 |
补充说明:
- ArrayList无参数构造时,elementData引用的首先是一个空数组,而不是默认长度为10的空数组。只有在第一次调用add方法时才会扩容到10。
- 指定数据初始化时,有一条判断句elementData.getClass() != Object[].class,当给定集合中的元素类型不是Object类时,会转化为Object类型。但是未必会成功转化,这是Java的一个bug,很少会被触发。
目前已知的会触发该bug的情景如下,
@Test
public void showBug() {
List<String> list = Arrays.asList("hello world");
Object[] objArray = list.toArray();
System.out.println(objArray.getClass().getSimpleName()); // 输出 String
objArray[0] = new Object(); // 报错
}
这个bug在Java 9中被解决。
添加元素及扩容
这两个放在一起讲的原因是新增元素时会分为两步,
- 判断是否需要扩容,如果需要则先对数组进行扩容
- 元素赋值
完整过程如下,之后会逐步进行解析
add方法解析
add方法源码,
/**
* Appends the specified element to the end of this list.
* @param e 添加到列表末尾的元素
* @return 添加成功返回true
*/
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
/**
* 在指定位置添加元素(相当于插入)
* @param index 添加元素的索引位置
* @param element 添加的元素
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public void add(int index, E element) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // Increments modCount!!
// 将index位置之后的元素拷贝到新的位置
System.arraycopy(elementData, index, elementData, index + 1, size - index);
// 新元素放到index位置
elementData[index] = element;
size++;
}
上面是add方法的两种调用形式,分别用于队尾添加元素和指定位置添加元素。
ensureCapacityInternal方法解析
ensureCapacityInternal方法判断容量是否合适,若不合适则进行扩容操作,
private void ensureCapacityInternal(int minCapacity) {
// 当elementData为无参数初始化时对应的数组,第一次调用add方法会先进行初始化。
// 默认最小长度为10
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
// 这个方法才是真正确保容积足够
ensureExplicitCapacity(minCapacity);
}
ensureCapacityInternal方法具体分为两步,
- 首先对elementData进行判断,如果是无参数初始化生成的ArrayList且elementData还是DEFAULTCAPACITY_EMPTY_ELEMENTDATA空数组,则首先进行初始化。最小长度默认为10。
- 如果之前已初始化,则调用ensureExplicitCapacity确保此次调用add方法时数组容积足够。
ensureExplicitCapacity方法解析
ensureExplicitCapacity方法源码,
private void ensureExplicitCapacity(int minCapacity) {
// 数组的修改次数+1(版本更新)
modCount++;
// 如果添加元素后长度超过当前容积,则扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
该方法的执行也分为两步,
- 更新ArrayList对象elementData的版本
- 判断当前容积是否足够,如果不足则调用grow方法扩容
grow方法解析
grow方法是实际对数组操作的方法,
/**
* 扩容数组,使其至少能够包含minCapacity个元素
* @param minCapacity the desired minimum capacity
*/
private void grow(int minCapacity) {
// 使用位运算,右移一位(相当于oldCapacity/2)
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 确保数组能够容纳至少minCapacity个元素,同时不超过最大元素限制
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 调用Arrays工具类,扩容得到新的数组(该工具类底层调用系统方法)
elementData = Arrays.copyOf(elementData, newCapacity);
}
grow方法的执行过程有3点需要注意,
- 扩容的规则不是翻倍,增量是原来容量的一半,即变为1.5倍。
- ArrayList数组默认的最大容纳量是Integer.MAX_VALUE个元素,超过该值则JVM不会再给分配空间。
- 新增数据时并没有对值进行严格校验,ArrayList添加的新值可以为null。
数组大小溢出及线程非安全
add方法中有数组大小溢出的意识,即扩容后数组大小在[0,Integer.MAX_VALUE]这个区间范围内。自定义类型时,这种意识是值得借鉴的。
在add方法执行到最后时,直接在数组上添加元素,
elementData[size++] = e;
该过程没有任何锁龙之,在多线程操作时是线程不安全的。
扩容的本质
grow方法中调用
Arrays.copyOf(element, newCapacity);
对数组进行扩容,本质上是数组之间的拷贝。具体分为两步,
- 创建一个符合预期容量的新数组
- 把老数组的元素拷贝到新数组中
Arrays.copyOf方法内调用的是底层的系统方法,
/**
* @param src 被拷贝的数组
* @param srcPos 从数组那里开始
* @param dest 目标数组
* @param destPos 从目标数组那个索引位置开始拷贝
* @param length 拷贝的长度
* 此方法没有返回值,通过 dest 的引用进行传值
*/
System.arraycopy(Object[] src, int srcPos, Object[] dest, int destPos, int length);
删除元素
删除元素的具体过程如下,
某个元素被删除后,为维护数组连续的结构,把删除元素之后的元素向前移动。
remove方法解析
/**
* 删除数组中第一个出现的指定元素,如果元素本身就不存在,数组不会改变。
* @param o 指定被删除的元素
* @return 如果数组中存在该元素,返回true;反之,false
*/
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值。
- 通过equals方法寻找元素的索引,如果元素不是基本类型,需要关注equals的具体实现。
fastRemove方法解析
该方法用于删除指定索引位置的元素,
/*
* Private remove method that skips bounds checking and does not
* return the value removed.
*/
private void fastRemove(int index) {
// 更新数组的版本信息
modCount++;
// index位置之后的元素都要前移
int numMoved = size - index - 1;
if (numMoved > 0)
// 从 index +1 位置开始被拷贝,拷贝的起始位置是 index,长度是 numMoved
System.arraycopy(elementData, index+1, elementData, index, numMoved);
//数组最后一个位置赋值 null,帮助 GC
elementData[--size] = null;
}
迭代器(内部类Itr)
自定义迭代器只需要实现Java中java.util.Iterator接口即可,ArrayList也是这样做的。ArrayList类中存在iterator方法,
public Iterator<E> iterator() {
return new Itr();
}
上面的代码中Itr类是ArrayList类的一个内部类,该类实现了Iterator接口,
private class Itr implements Iterator<E> {
......
}
迭代器中有三个重要的参数,
类型 | 参数 | 说明 |
---|---|---|
int | cursor | 迭代过程中下一个元素的位置,默认从0开始 |
int | lastRet | 新增场景:表示上一次迭代过程中,索引的位置;删除场景:为 -1。 |
int | expectModCount=modCount | expectedModCount 表示迭代过程中,期望的版本号;modCount 表示数组实际的版本号。 |
迭代器一般都要有三个方法,
方法名 | 说明 |
---|---|
hasNext | 是否存在值能够迭代 |
next | 返回可以迭代的值 |
remove | 删除当前迭代的值 |
这三个方法都是Itr类的成员方法。
hasNext方法解析
public boolean hasNext() {
//cursor 表示下一个元素的位置,size 表示实际大小,如果两者相等,说明已经没有元素可以迭代了,如果不等,说明还可以迭代
return cursor != size;
}
next方法解析
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;
// 返回元素值,同时此处会更新lastRet的值
return (E) elementData[lastRet = i];
}
next方法的执行过程分为两步,
- 检查是否能够继续迭代
- 找到迭代的值,并更新cursor为下一次迭代做准备
- 更新lastRet的值为当前位置i,不更新的话该类的remove方法将永远只能抛出异常
checkForComodification方法用于比较版本号,
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
remove方法解析
public void remove() {
// 如果lastRet小于0,说明数组该位置的元素已经被删除
if (lastRet < 0)
throw new IllegalStateException();
//迭代过程中,判断版本号有无被修改,有被修改,抛 ConcurrentModificationException 异常
checkForComodification();
try {
// 此处是调用ArrayList的remove方法删除该位置元素
ArrayList.this.remove(lastRet);
cursor = lastRet;
// -1 表示元素已经被删除,这里也防止重复删除
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
remove方法有两点注意,
- lastRet = -1是为了在当前位置防止重复删除
- 元素删除成功后,数组的modCount会更新,此处吧expectedModCount重新赋值,下次使用next方法进行迭代时二者的值是一致的
线程安全
当ArrayList在多线程中以共享变量的方式存在时才会有线程安全问题,当ArrayList是方法内部的局部变量时,是不存在线程安全问题的。
ArrayList线程非安全的原因是对elementData、size和modCount进行操作的过程中没有加锁,而且这些变量并不是volatile关键字修饰的(CPU不可见),所以当多线程同时操作时会出现值被覆盖的问题。
解决线程安全问题的方法是使用Collections.synchronizedList方法将原本的List对象包装为线程安全的SynchronizedList类对象。SynchronizedList通过在每个方法上加锁实现,但是降低了性能。
synchronizedList方法两种形式,
/**
* Returns a synchronized (thread-safe) list backed by the specified list.
* @param list 需要被改造成线程安全的List对象
*/
public static <T> List<T> synchronizedList(List<T> list) {
return (list instanceof RandomAccess ?
new SynchronizedRandomAccessList<>(list) :
new SynchronizedList<>(list));
}
/**
* Collections工具类 synchronizedList方法
* @param list 需要被改造成线程安全的List对象
* @param mutex Object on which to synchronize(相当于锁对象)
* @return 如果数组中存在该元素,返回true;反之,false
*/
static <T> List<T> synchronizedList(List<T> list, Object mutex) {
return (list instanceof RandomAccess ?
new SynchronizedRandomAccessList<>(list, mutex) :
new SynchronizedList<>(list, mutex));
}
如果没有指定mutex参数,则默认使用Collections类中已经定义好的一个Object对象作为SynchronizedList类的锁。
其中SynchronizedList类是Collections类的内部类,
static class SynchronizedList<E> extends SynchronizedCollection<E> implements List<E> {
该类的方法以add方法为例,
public void add(int index, E element) {
synchronized (mutex) {
list.add(index, element);
}
}
总结
ArrayList围绕底层数组结构,各个API都是对数组的操作进行封装。