ArrayList 源码解析和设计思路
整体架构
ArrayList 整体架构比较简单,就是一个数组结构,比较简单,如下图:
图中展示是长度为 10 的数组,从 1 开始计数,index
表示数组的下标
,从 0 开始计数,elementData
表示数组本身
,源码中除了这两个概念,还有以下三个基本概念:
DEFAULT_CAPACITY
表示数组的初始大小
,默认是10
,这个数字要记住;size
表示当前数组的大小
,类型 int ,没有使用 volatile 修饰,非线程安全的;modCount
统计当前数组被修改的版本次数
,数组结构有变动,就会 +1.
类注释
看源码,首先要看类注释,我们看看类注释上面都说了什么:
-
允许 pull null 值,会自动扩容
-
size、isEmpty、get、set、add 等方法时间复杂度都是 O(1)
-
非线程安全的,多线程情况下,推荐使用线程安全类:
Collection#synchronizedList
-
增强 for 循环,或者使用迭代器迭代的过程中,如果数组的大小被改变,会快速失败,抛出异常
除了上述注释中提到的 4 点,初始化、扩容的本质、迭代器等问题也经常被问,接下来我们从源码出发,一一解析。
源码解析
初始化
有三种初始化方法:无参数直接初始化、指定大小初始化、指定初始数据初始化,源码如下:
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 无参数直接初始化,数组大小为空
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
ArrayList 无参构造器初始化时,默认大小是空数组,并不是大家常说的 10,10 是在第一次 add 的时候扩容的数组值
transient Object[] 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);
}
}
// 指定初始数据初始化
public ArrayList(Collection<? extends E> c) {
Object[] a = c.toArray();
if ((size = a.length) != 0) {
if (c.getClass() == ArrayList.class) {
elementData = a;
} else {
elementData = Arrays.copyOf(a, size, Object[].class);
}
} else {
// 给定数据无值,默认空数组
elementData = EMPTY_ELEMENTDATA;
}
}
新增和扩容实现
新增就是往数组中添加元素,主要分成两步:
- 判断是否需要扩容,如果需要执行扩容操作;
- 直接赋值。
两步源码体现如下:
public boolean add(E e) {
// 确保数组大小是否足够,不够执行扩容,size 为当前数组大小
ensureCapacityInternal(size + 1); // Increments modCount!!
// 直接赋值,线程不安全的
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
// 计算所需的容量
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
// 确保容量足够用
private void ensureExplicitCapacity(int minCapacity) {
// 记录数组修改次数
modCount++;
// 如果期望的容量大于当前数组的长度,则扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
// 扩容,并把现有数据拷贝到新的数组里面去
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
// oldCapacity >> 1 是把 oldCapacity 除以 2 的意思
// 是原来容量大小 + 容量大小的一半,即1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 如果扩容后的值 < 我们的期望值,扩容后的值就等于我们的期望值
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 如果扩容后的值 > jvm 所能分配的数组的最大值,那么就用 Integer 的最大值
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 拷贝数组
elementData = Arrays.copyOf(elementData, newCapacity);
}
-
扩容的规则并不是翻倍,是原来容量 + 容量大小的一半,即扩容后的大小是原来容量的 1.5 倍
-
ArrayList 中的数组的最大值是 Integer.MAX_VALUE,超过这个值,JVM 就不会给数组分配内存空间了
-
新增时,并没有对值进行严格的校验,所以 ArrayList 是允许 null 值的
-
扩容完成后,赋值是非常简单的,没有任何锁控制,所以这里的操作是线程不安全的
扩容的本质
扩容是通过这行代码来实现的: Arrays.copyOf(elementData, newCapacity)
,这行代码描述的本质是数组之间的拷贝,扩容是会先新建一个符合我们预期容量的新数组,然后把老数组的数据拷贝过去,底层是通过 System.arraycopy
方法进行拷贝,此方法是 native 方法
删除
ArrayList 删除元素有很多种方式,比如根据数组索引删除、根据值删除或批量删除等等,原理和思路都差不多,我们选取根据值删除方式来分析下源码:
public boolean remove(Object o) {
// 如果要删除的值是 null,找到数组中第一个值为null的删除
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
// 如果要删除的值不为 null,找到第一个和要删除的值相等的删除
for (int index = 0; index < size; index++)
// 这里是根据 equals 来判断值相等的,相等后再根据索引位置进行删除
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
private void fastRemove(int index) {
// 数组修改次数+1
modCount++;
// 删除后需要把后面的元素往前挪动,计算移动的数量
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; // clear to let GC do its work
}
- 新增的时候没有对 null 进行校验,所以删除的时候也是允许删除 null 值的
- 找到值在数组中的索引位置,是通过
equals
来判断的,如果数组元素不是基本类型,需要关注 equals 的具体实现 - 某一个元素被删除后,为了维护数组结构,会把数组后面的元素往前移动
时间复杂度
操作 | 时间复杂度 |
---|---|
读取 get(index) | 根据下标直接查数组,O(1) |
添加 add(E) | 直接尾部添加,O(1) |
指定位置添加 add(index, E) | 查询后添加,涉及元素挪动,O(n) |
删除 remove(E) | 删除指定元素,需要遍历查找元素下标,删除后需要挪动后面元素,O(n) |
线程安全
ArrayList 作为共享变量使用时,会出现线程安全问题。
ArrayList 有线程安全问题的本质,是因为 ArrayList 自身的 elementData、size、modCount 在进行操作时,都没有加锁,而且这些变量的类型并非是可见的(volatile),座椅如果多个线程对这些变量进行操作时,可能会有值被覆盖的情况。
类注释中推荐我们使用 Collections#synchronizedList 来保证线程安全,SynchronizedList
是通过在每个方法上面加上锁来实现,虽然实现了线程安全,但是性能大大降低
,具体实现源码:
public E get(int index) {
synchronized (mutex) {return list.get(index);}
}
public E set(int index, E element) {
synchronized (mutex) {return list.set(index, element);}
}
public void add(int index, E element) {
synchronized (mutex) {list.add(index, element);}
}
public E remove(int index) {
synchronized (mutex) {return list.remove(index);}
}