目录标题
- 一、ArrayList
- 1.1 整体架构
- 1.2 类注释
- 二、 源码解析
- 2.1 如何初始化
- 2.2 新增和扩容实现
- 2.2.1 具体流程
- 2.3 Arrays.copyOf(elementData, newCapacity)
- 2.4 删除
- 2.5 时间复杂度
- 2.6 迭代器
- 2.6.1 hasNext()
- 2.6.2 next()
- 2.6.3 remove()
一、ArrayList
1.1 整体架构
ArrayList 整体架构比较简单,就是一个数组结构,比较简单,如下图:
图中展示是长度为 10 的数组,从 1 开始计数,index 表示数组的下标,从 0 开始计数,
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
// ... //
// DEFAULT_CAPACITY 表示数组的初始大小,默认是 10,这个数字要记住;
private static final int DEFAULT_CAPACITY = 10;
// 用于空实例的共享空数组实例
private static final Object[] EMPTY_ELEMENTDATA = {};
// 用于提供默认大小的实例的共享空数组实例
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// elementData 表示数组本身;
transient Object[] elementData;
// size 表示当前数组的大小,类型 int,没有使用 volatile 修饰,非线程安全的;
private int size;
}
源码中还有一个非常重要的概念:
在父类AbstractList上,定义了modCount
属性,用于记录数组修改的次数。modCount
统计当前数组被修改的版本次数,数组结构有变动,就会 +1。
1.2 类注释
接下来,让我们看一下类的注释,我就大致总结一下:
- 可以存放所有元素,包括null;
- size、isEmpty、get、set、iterator、add等方法的时间复杂度为O(1),即添加 n 个元素需要 O(n) 时间;
- 随着元素被添加到 ArrayList,它的容量会自动增长;
- 非线程安全的,多线程情况下,则应使用 Collections.synchronizedList 方法“包装”该列表;
- 此类的 iterator 和 listIterator 方法返回的迭代器是快速失败的,会抛出异常。
二、 源码解析
2.1 如何初始化
有三种初始化方式:直接初始化、指定大小初始化、指定初始数据初始化
,源码如下:
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
private static final Object[] EMPTY_ELEMENTDATA = {};
// 无参数直接初始化,数组大小为空
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
// 指定初始化大小初始化
// 该构造方法很简洁,接收了一个初始容量initialCapacity,并判断是否合法(大于等于0),大于等于0时,初始化了数组的数组内容。
public ArrayList(int initialCapacity) {
// 初始容量大于0
if (initialCapacity > 0) {
// 创建一个大小为initialCapacity的对象数组赋给elementData。
this.elementData = new Object[initialCapacity];
// // 初始容量为0
} else if (initialCapacity == 0) {
// 则将EMPTY_ELEMENTDATA赋给elementData。
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
// 指定初始化数据的初始化
public ArrayList(Collection<? extends E> c) {
// 传入的 c 转化为数组
elementData = c.toArray();
// 如果给定的集合 c 有数值
if ((size = elementData.length) != 0) {
// 如果集合元素的类型不是 Object 类型,将会转化为 Object
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// 集合 c 的大小为空,则设置元素数组为空
this.elementData = EMPTY_ELEMENTDATA;
}
}
补充:ArrayList 无参构造器初始化时,默认大小是空数组,并不是大家常说的 10,10 是在第一次 add 的时候扩容的数组值。
2.2 新增和扩容实现
新增就是往数组中添加元素,主要分成两步:
- 判断是否需要扩容,如果需要则执行扩容操作;
- 直接赋值。
源码如下:
/**
* 新增
*/
public boolean add(E e) {
// 确保数组的大小是否足够,不够则执行扩容,size 为当前数组的大小
ensureCapacityInternal(size + 1); // Increments modCount!!
// 直接赋值,线程不安全
elementData[size++] = e;
return true;
}
扩容(ensureCapacityInternal)的源码
private void ensureCapacityInternal(int minCapacity) {
// 判断元素数组是否为空数组
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
// 取较大值
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
// 确保容积足够
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
// 记录数组被修改
modCount++;
// 如果期望的最小容量大于目前的数组的长度,则扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
// 扩容,并把现有数据拷贝到新的数组里面去
private void grow(int minCapacity) {
// 旧容量
int oldCapacity = elementData.length;
// 新容量为旧容量的1.5倍;>> 1是除以2的意思
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);
}
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,超过这个值,JVM 就不会给数组分配内存空间;
- 新增时,并没有对值进行严格的校验,所以 ArrayList 是允许 null 值的;
2.2.1 具体流程
扩容机制主要发生在集合中添加元素的时候,由add()方法的分析可知添加前必须确保集合的容量能够放下添加的元素。主要经历了以下几个阶段:
-
在add()方法中调用ensureCapacityInternal(size + 1)
方法来确定集合确保添加元素成功的最小集合容量minCapacity的值。「 参数为size+1 」,代表的含义是如果集合添加元素成功后,集合中的实际元素个数。换句话说,集合为了确保添加元素成功,那么集合的最小容量minCapacity应该是「 size+1 」。在ensureCapacityInternal方法中,首先判断elementData是否为默认的空数组,如果是,minCapacity为minCapacity与集合默认容量大小中的较大值。 - 调用
ensureExplicitCapacity(minCapacity)
方法来确定集合为了确保添加元素成功是否需要对现有的元素数组进行扩容。首先将结构性修改计数器加一;然后判断minCapacity与当前元素数组的长度的大小,如果minCapacity比当前元素数组的长度的大的时候需要扩容,进入第三阶段。 - 如果需要对现有的元素数组进行扩容,则调用
grow(minCapacity)
方法,参数minCapacity表示集合为了确保添加元素成功的最小容量。在扩容的时候,首先将原元素数组的长度增大1.5倍(oldCapacity + (oldCapacity >> 1))
,然后对扩容后的容量与minCapacity进行比较:新容量小于minCapacity,则将新容量设为minCapacity;新容量大于minCapacity,则指定新容量。最后将旧数组拷贝到扩容后的新数组中。
2.3 Arrays.copyOf(elementData, newCapacity)
Arrays.copyOf(elementData, newCapacity)
这行代码描述的本质是数组之间的拷贝,扩容是会先新建一个符合我们预期容量的新数组,然后把老数组的数据拷贝过去:
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
2.4 删除
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) {
// 记录数组改动的次数
modCount++;
// numMoved 表示删除 index 位置的元素后,需要从 index 位置向后移动多少个元素到前面去
int numMoved = size - index - 1; // 减一是因为,size 从 1 开始算起,index 从 0 算起
if (numMoved > 0)
// 从 index + 1 位置开始拷贝,起始位置为 index ,长度为 numMoved
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
// 数组最后一个位置赋值 null
elementData[--size] = null; // clear to let GC do its work
}
remove(int index)
方法和上面也差不多,这里就不介绍了。
2.5 时间复杂度
从我们上面新增或删除方法的源码解析,对数组元素的操作,只需要根据数组索引,直接新增和删除,所以时间复杂度是 O (1)。
2.6 迭代器
迭代器一般来说有三个方法:hasNext
还有没有值可以迭代;next
如果有值可以迭代,迭代的值是多少;remove
删除当前迭代的值。
几个重要参数:
int cursor; // 迭代过程中,下一个元素的位置,默认从 0 开始。
int lastRet = -1; // 最后一个要返回元素的下标 没有元素返回 -1
int expectedModCount = modCount; // 表示迭代过程中,期望的版本号;modCount 表示
2.6.1 hasNext()
public boolean hasNext() {
// cursor 表示下一个元素的位置
// size 表示实际大小,如果两者相等,说明已经没有元素可以迭代了,如果不等,说明还可以迭代
return cursor != size;
}
2.6.2 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();
}
综上,next 方法就干了两件事情,第一是检验能不能继续迭代
,第二是找到迭代的值,并为下一次迭代做准备(cursor+1)
。
2.6.3 remove()
public void remove() {
// 表示数组已经删除完了
if (lastRet < 0)
throw new IllegalStateException();
// 判断版本号是否被修改
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
// -1 表示元素已经被删除
lastRet = -1;
// 删除元素时,modCount 的值已经发生了变化,此时赋给 expectedModCount
// 下次迭代时,两者的值为一致的了
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
lastRet = -1 的操作目的,是防止重复删除操作;删除元素成功,数组当前 modCount 就会发生变化,这里会把 expectedModCount 重新赋值,下次迭代时两者的值就会一致了。