源码阅读之 ArrayList 类
源码阅读之 ArrayList 类
1 基本结构
以下是 ArrayList 类基础部分的简单结构,API 相关的未列出:
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
// 序列化用
private static final long serialVersionUID = 8683452581122892189L;
// 默认初始容量为 10
private static final int DEFAULT_CAPACITY = 10;
// 简称 EE,在 有参 构造 ArrayList 的时候用来表示空数组
private static final Object[] EMPTY_ELEMENTDATA = {};
// 简称 DEE,在 无参 构造 ArrayList 的时候用来表示空数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// elementData 存储实际的数据、size 表示数组中元素的数量
transient Object[] elementData; // non-private to simplify nested class access
private int size;
// 三个构造器
public ArrayList(int initialCapacity);
public ArrayList();
public ArrayList(Collection<? extends E> c);
// 三个内部类
private class Itr implements Iterator<E> {}
private class ListItr extends Itr implements ListIterator<E> {}
private class SubList extends AbstractList<E> implements RandomAccess {}
// 记录数据被结构性修改的次数
protected transient int modCount = 0;
// ArrayList 最大大小
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
}
2 初始化
详情见 EMPTY_ELEMENTDATA 和 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 章节,其中涉及到的一个思想就是——懒(lazy),尤其是在创建空的 ArrayList
的时候。
- 如果创建的
ArrayList
一开始的时候是空的,那么它的elementData
一定引用一个(两个)共同的空数组,直到向其中添加元素它才会初始化,并且elementData
的长度一定大于等于 10。 - 如果通过已有的集合来创建
ArrayList
,那么其elementData
的长度等同于已有集合的长度。
2.1 从指定容器创建
这是一个比较坑的构造,不太符合我们的思维习惯:
public ArrayList(Collection<? extends E> c) {
Object[] a = c.toArray();
// 从已有集合构建时
if ((size = a.length) != 0) {
if (c.getClass() == ArrayList.class) {
// 如果参数也是 ArrayList,直接引用原数组!!!
elementData = a;
} else {
// 如果对象不是 ArrayList,则使用 copyOf 方法,是浅拷贝!!!
elementData = Arrays.copyOf(a, size, Object[].class);
}
} else {
// replace with empty array.
elementData = EMPTY_ELEMENTDATA;
}
}
从旧的容器构建新的容器,新容器里面的元素仍然会引用旧的元素:
public static void testCopy() {
class User {
String name = "Hello";
}
User user = new User();
ArrayList<User> list1 = new ArrayList<>();
list1.add(user);
ArrayList<User> list2 = new ArrayList<>(list1);
System.out.println(list2.get(0).name); // Hello
list1.get(0).name = "hi";
System.out.println(list2.get(0).name); // hi
}
3 自动扩容
由于用数组 elementData
存储数据,ArrayList
需要在每次添加元素的时候都确认以下数组是否足够大,如果数组长度不足以存储下新的数据,则需要增加数组长度。
3.1 初始状态
以下讨论中假定 ArrayList
的初始状态是空(也就是说其中的 elementData
引用一个空数组)。
3.2 add(E e) 扩容
以下是 add
方法的调用链,由于大都是 ArrayList
内部的调用,所以不太明显,但是调用的顺序可以清楚地看到:
1 调用 add
方法直接添加元素:
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
1.1 add
方法调用的第一个方法就是 ensureCapacityInternal
,传入的实参是当前的数组中元素的数量 + 1,而虚参名称为 minCapacity
,指意明确:
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
1.1.1 calculateCapacity
方法主要用来计算合理的最小容量:
private static int calculateCapacity(Object[] elementData, int minCapacity) {
// 如果当前 ArrayList 为空,则取 DEFAULT_CAPACITY(10) 和 minCapacity(这里是 1)的最大值
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
从这里可以看出来,一旦 ArrayList
中实际存储了元素,那么它 elementData
的长度一定会大于等于 10
1.1.2 ensureExplicitCapacity
方法主要用来记录 modCount
的增加(扩容——更准确说是添加元素,因为只有添加了元素才会扩容,是一种结构性的修改,所以 modCount
需要增加)。
// overflow-conscious code
注释下的代码即是扩容的入口了,
private void ensureExplicitCapacity(int minCapacity) {
// 这里的 minCapacity 已经是 >= 10 的一个值了
modCount++;
// overflow-conscious code 表示考虑了溢出问题
if (minCapacity - elementData.length > 0) // 如果当前需要的最小容量大于当前 elementData 的长度,那么则需要扩容,参数为 minCapacity
grow(minCapacity);
}
1.1.2.1 grow
方法就像它的名字一样,成长、变大:
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1); // 新的容量为 oldCapacity / 2 + oldCapacity = (3/2)* oldCapacity
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity; // 新的容量足够了
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity); // 新的容量比默认最大 ArrayList 长度还大
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
1.1.2.1.1 可能的 hugeCapacity
调用,这个方法主要用于处理溢出(前面用减法比较的 newCapacity
和 MAX_ARRAY_SIZE
大小,可能会溢出)
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError(); // 如果在初始化 ArrayList 的时候指定一个 MAX_ARRAY_SIZE 的
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
1.1.2.1.2 Arrays.copyOf
方法
Arrays.copy
调用 System.arraycopy
,后者则调用了本地方法。
前者会自动创建并且返回新的数组,后者则是需要指定复制后的接受数组对象。扩容时一定需要创建新的数组,所以使用 Arrays.copy 方便(虽然它最终还是回去调用 System.arraycopy
),但是在已经扩容后插入元素的操作中进行批量元素复制的话就不要使用 Arrays.copy 方法了,因为这样又会创建一个新的数组!
3.3 add(index, e) 扩容
以下是源码,可以看到其无非就是多了一个 index
合法性的判断和插入位置之后的元素移动:
public void add(int index, E element) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // Increments modCount!!
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
3.4 addAll() 扩容
没啥好说的了,原理基本一致。但是它不用一次次地扩容,而是一次性扩让到指定大小。
public boolean addAll(Collection<? extends E> c) {
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew); // Increments modCount
System.arraycopy(a, 0, elementData, size, numNew);
size += numNew;
return numNew != 0;
}
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);
System.arraycopy(a, 0, elementData, index, numNew);
size += numNew;
return numNew != 0;
}
4 手动扩容
开发者为我们留下了这样的一个公有方法,但是自身并未使用:
// Increases the capacity of this ArrayList instance, if necessary, to ensure that it can hold at least the number of elements specified by the minimum capacity argument.
// Params:
// minCapacity – the desired minimum capacity
public void ensureCapacity(int minCapacity) {
int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
// any size if not default element table
? 0
// larger than default for default empty table. It's already
// supposed to be at default size.
: DEFAULT_CAPACITY;
if (minCapacity > minExpand) {
ensureExplicitCapacity(minCapacity);
}
}
通过观察自动扩容机制,我们可以看到,如果写出类似如下的代码:
for (int i = 0; i < 1000; i++) {
list.add(i);
}
调用 ensureCapacity 它,并且指定实参为 1000,免去自动扩容过程中重复地创建新的数组(Arrays.copyOf 方法扩容)。
5 remove
remove()
方法也有两个版本,一个是remove(int index)
删除指定位置的元素,另一个是remove(Object o)
删除第一个满足o.equals(elementData[index])
的元素。删除操作是add()
操作的逆过程,需要将删除点之后的元素向前移动一个位置。需要注意的是为了让GC起作用,必须显式的为最后一个位置赋null
值。
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
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
return oldValue;
}
对象能否被GC的依据是是否还有引用指向它,上面代码中如果不手动赋 null
值,除非对应的位置被其他元素覆盖,否则原来的对象就一直不会被回收。
6 EE 和 DEE
EMPTY_ELEMENTDATA
和 DEFAULTCAPACITY_EMPTY_ELEMENTDATA
,最开始看到这两个静态变量是感到非常疑惑的,虽然说看在构造空 ArrayList
的时候直接把这两个变量赋值给新的 ArrayList
对象中的 elementData
很方便,但是感觉大可不必,直接创建一个新的空数组然后赋值给 elementData
不好吗?
以下是 java7
中 ArrayList
的构造器代码(时间问题,没来得及考证。。。):
public ArrayList(int initialCapacity) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
this.elementData = new Object[initialCapacity];
}
public ArrayList() {
super();
this.elementData = EMPTY_ELEMENTDATA;
}
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
size = elementData.length;
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
}
以下是 java8 中 ArrayList 的构造器代码:
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
// 在这里增加了容量为 0 的判断
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
public ArrayList() {
// 创建的时候是一个空数组,长度不是默认为 10
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
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 {
// replace with empty array. 这是官方的注释
elementData = EMPTY_ELEMENTDATA;
}
}
首先,为什么创建一个空的(无参构造) ArrayList 不直接使用 elementData = new Object[initialCapacity]
呢?
因为创建一个对象需要消耗大量的资源!这点很容易理解了,除非这个 ArrayList 中的 elementData 真的要存储东西,不然我就让它先引用所有 ArrayList 全局共享的一个空数组(DEFAULTCAPACITY_EMPTY_ELEMENTDATA
),当真的需要 elementData 存储数据的时候,我再给它创建单独的对象(add 系方法中提现),这应该也是一种懒加载(lazy)的体现。
7 迭代与 fail-fast
众所周知,使用迭代器迭代过程中是不允许对容器进行结构性修改(增加、减少元素)的(使用迭代器的 remove
方法除外),modCount
字段就是专门来应对这个问题的。
以下是 ArrayList.Iterator 的基本结构:
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
Itr() {}
public boolean hasNext()
public E next()
public void remove()
public void forEachRemaining(Consumer<? super E> consumer)
final void checkForComodification()
}
ArrayList也采用了快速失败的机制,通过记录modCount参数来实现。在面对并发的修改时,迭代器很快就会完全失败,这点具体体现在迭代器的 next 方法中:
public E next() {
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];
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
而不是冒着在将来某个不确定时间发生任意不确定行为的风险。
参看这篇文章:https://www.javatpoint.com/fail-fast-and-fail-safe-iterator-in-java
还有下面一小段 demo:
public static void testConcurrentModify() {
ArrayList<Integer> list = new ArrayList<>(Arrays.asList(1,2,3,4,5,6));
Iterator<Integer> iterator = list.iterator();
int count = 0;
while (iterator.hasNext()) {
Integer next = iterator.next();
if (count == 0) {
list.remove(0);
count = 1;
}
System.out.println(next);
// 输出 1 后抛出异常
}
}
public static void testConcurrentModifyForEachRemaining() {
ArrayList<Integer> list = new ArrayList<>(Arrays.asList(1,2,3,4,5,6));
Iterator<Integer> iterator = list.iterator();
int count = 0;
iterator.forEachRemaining(e -> {
System.out.println(e);
list.remove(0);
});
// 输出 1 后抛出异常
}
作者:locustree
出处:https://www.cnblogs.com/locustree/p/16201224.html
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· DeepSeek在M芯片Mac上本地化部署
· 葡萄城 AI 搜索升级:DeepSeek 加持,客户体验更智能