简介
ArrayList 是一个数组列表,相当于动态数组。与Java中的数组相比,它的容量能动态增长。
以下分析基于corretto-1.8.0_282版本。
继承关系
- 实现了RandomAccess接口,代表其可进行快速随机访问,因为底层使用数组储存元素,所以可直接通过下标访问元素。
- 实现了Cloneable接口,可以通过.clone()方法克隆出一个实例。
- 实现了Serializable接口,可以被序列化。
- 实现了List接口,提供了添加、删除、遍历等操作。
属性
DEFAULT_CAPACITY
/**
* 默认的初始容量
* 实例化ArrayList时,如果不指定初始大小,则以此值作为ArrayList的初始容量
*/
private static final int DEFAULT_CAPACITY = 10;
EMPTY_ELEMENTDATA
/**
* 空实例使用的共享空数组
* 若初始化时指定大小为0,即new ArrayList(0),则将elementData赋值为EMPTY_ELEMENTDATA
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
DEFAULTCAPACITY_EMPTY_ELEMENTDATA
/**
* 以默认容量初始化的ArrayList,其elementData会初始
* 化为DEFAULTCAPACITY_EMPTY_ELEMENTDATA。
* 待添加第一个元素时,会以DEFAULT_CAPACITY的大小开辟数组空间
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
elementData
/**
* 存储ArrayList中元素的数组。
* ArrayList的容量是此数组的长度。
* transient关键字说明不会对此字段进行序列化
* ArrayList通过自定义writeObject()方法对其进行序列化,通过
* readObject()方法对其进行反序列化
*/
transient Object[] elementData;
size
/**
* ArrayList的大小(它包含的元素数)
*/
private int size;
MAX_ARRAY_SIZE
/**
* 要分配的数组的最大大小,申请大于此大小的数组可能
* 会抛出异常OutOfMemoryError: Requested array size exceeds VM limit
*/
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
构造方法
ArrayList()
/**
* 按默认容量实例化的构造函数
* 此构造函数不会开辟数组空间,而是等到插入第一个元素时才真正开辟数组空间
*/
public ArrayList() {
// 将元素数组赋值为DEFAULTCAPACITY_EMPTY_ELEMENTDATA
// 即实例化时先赋值为空数组,待插入第一个元素时才真正开辟数组空间
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
ArrayList(int initialCapacity)
/**
* 按指定容量实例化的构造函数
* 此构造函数会按传入的容量开辟数组空间
*/
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
// 若指定容量大于0,则按指定容量初始化元素数组
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
// 若指定容量等于0,则将元素数组赋值为EMPTY_ELEMENTDATA
this.elementData = EMPTY_ELEMENTDATA;
} else {
// 若指定容量小于0,抛出异常
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
ArrayList(Collection<? extends E> c)
/**
* 按给定集合实例化的构造函数
*/
public ArrayList(Collection<? extends E> c) {
// 传入集合转为数组
Object[] a = c.toArray();
if ((size = a.length) != 0) {
if (c.getClass() == ArrayList.class) {
// 若集合类型为ArrayList,则toArray()方法返回的
// 一定是Object[]类型,所以直接赋值给elementData
elementData = a;
} else {
// 否则,toArray()方法返回的不一定是Object[]类型,需要做类型转换
elementData = Arrays.copyOf(a, size, Object[].class);
}
} else {
// 若集合大小为0,则将元素数组赋值为EMPTY_ELEMENTDATA
elementData = EMPTY_ELEMENTDATA;
}
}
方法
add(E e) 方法
将元素添加至列表末尾。
public boolean add(E e) {
// 确保元素数组有足够的空间,同时修改次数(modCount)+1
ensureCapacityInternal(size + 1);
// 列表末尾添加元素,同时列表大小+1
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
/**
* 计算所需要的大小
* 如果是第一次添加元素,则取DEFAULT_CAPACITY和minCapacity之中的最大值
* 否则返回minCapacity,这里是当前列表大小+1
*/
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
/**
* 判断elementData大小是否满足传入的容量,不满足则进行扩容
*/
private void ensureExplicitCapacity(int minCapacity) {
// 修改次数+1,用于做快速失败(fail-fast)检查
modCount++;
// 若需要的容量大于elementData的大小,则对elementData进行扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
/**
* 对elementData进行扩容
*/
private void grow(int minCapacity) {
// 保存旧容量
int oldCapacity = elementData.length;
// 新容量设置为旧容量的1.5倍(右移一位相当于除以2)
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 若1.5倍还是小于需要的容量minCapacity,则将新容量设置为minCapacity
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 如果新容量超过定义的最大数组容量MAX_ARRAY_SIZE,则判断需要的容量是否也大于定义的最大数组容量
// 若需要的容量也大于MAX_ARRAY_SIZE则将新容量设置为Integer.MAX_VALUE,否则将新容量设置为MAX_ARRAY_SIZE
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 以新容量对elementData进行扩容
elementData = Arrays.copyOf(elementData, newCapacity);
}
private static int hugeCapacity(int minCapacity) {
// 如果minCapacity小于0,说明minCapacity超过Integer.MAX_VALUE发生溢出,抛出异常
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
- 检查是否需要扩容,同时将修改次数(modCount)+1。
- 若为第一次添加元素,此时elementData 等于 DEFAULTCAPACITY_EMPTY_ELEMENTDATA,则将elementData初始化为默认大小DEFAULT_CAPACITY。
- 新容量为旧容量的1.5倍,若调整为1.5倍之后仍然小于所需容量,则按需要的容量为准。
- 如果新容量超过定义的最大数组容量MAX_ARRAY_SIZE,则判断需要的容量是否也大于定义的最大数组容量,若需要的容量也大于MAX_ARRAY_SIZE则将新容量设置为Integer.MAX_VALUE,否则将新容量设置为MAX_ARRAY_SIZE。
- 使用新的容量创建数组并把元素复制到新数组中。
- 在列表末尾添加元素,同时列表大小加一。
add(int index, E element) 方法
在指定索引处添加元素。
public void add(int index, E element) {
// 检查索引是否合法
rangeCheckForAdd(index);
// 确保元素数组有足够空间
ensureCapacityInternal(size + 1);
// 从指定位置将所有元素向后移动一位
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
// 将元素插入指定位置
elementData[index] = element;
// 列表大小+1
size++;
}
private void rangeCheckForAdd(int index) {
// 若指定下标小于0或大于当前列表大小,抛出异常
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
- 检查指定下标是否合法。
- 确保元素数组有足够的空间,同时修改次数(modCount)+1。
- 若指定下标不在列表末尾,则需要从此下标处将所有元素向后移动一位。
- 将元素插入指定位置。
- 元素个数+1。
addAll(Collection<? extends E> c) 方法
将给定集合中的所有元素添加到列表末尾。
public boolean addAll(Collection<? extends E> c) {
// 集合转数组
Object[] a = c.toArray();
int numNew = a.length;
// 确保元素数组有足够的空间,同时修改次数(modCount)+1
ensureCapacityInternal(size + numNew);
// 将集合的数组复制到elementData的末尾
System.arraycopy(a, 0, elementData, size, numNew);
// 修改元素个数
size += numNew;
return numNew != 0;
}
- 调用传入集合的toArray()方法将其转为数组。
- 确保元素数组有足够的空间,同时修改次数(modCount)+1。
- 将集合的数组复制到elementData的末尾。
- 修改元素个数。
addAll(int index, Collection<? extends E> c) 方法
将给定集合中的所有元素添加进列表指定索引处,即求两个集合的并集。
public boolean addAll(int index, Collection<? extends E> c) {
// 确保索引在[0, size](闭区间)之间
rangeCheckForAdd(index);
// 集合转数组
Object[] a = c.toArray();
int numNew = a.length;
// 确保元素数组有足够空间
ensureCapacityInternal(size + numNew);
// 获取需要移动元素的个数,若指定索引在列表末尾,则不需要进行移动
int numMoved = size - index;
if (numMoved > 0)
System.arraycopy(elementData, index, elementData, index + numNew,
numMoved);
// 将集合数组复制到elementData的指定位置
System.arraycopy(a, 0, elementData, index, numNew);
// 修改元素个数
size += numNew;
return numNew != 0;
}
- 检查索引是否合法。
- 调用传入集合的toArray()方法将其转为数组,获取其元素个数numNew。
- 确保元素数组有足够的空间,同时修改次数(modCount)+1。
- 若索引不是列表末尾,则需要将此索引之后的所有元素向后移动numNew位。
- 将传入集合的元素数组复制到elementData的指定位置。
- 列表大小+numNew。
get(int index) 方法
获取指定索引处的元素。
public E get(int index) {
// 检查索引是否越界
rangeCheck(index);
return elementData(index);
}
/**
* 检查索引是否越界
*/
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
/**
* 返回指定索引处的元素,并按照泛型做强制类型转换
*/
E elementData(int index) {
return (E) elementData[index];
}
- 检查索引是否越界,这里只检查了上界,下界交由JVM检查,具体原因不明这里有一篇帖子说明了为什么不检查下界 Why does ArrayList#rangeCheck not check if the index is negative?。
- 返回指定索引处的元素,并根据泛型做强制类型转换。
set(int index, E element) 方法
修改指定索引处的元素。
public E set(int index, E element) {
// 检查索引是否越界
rangeCheck(index);
// 保存旧元素,作为方法返回值
E oldValue = elementData(index);
// 更新元素
elementData[index] = element;
return oldValue;
}
- 检查索引是否越界。
- 保存旧元素,作为方法返回值。
- 更新指定索引处的元素。
remove(int index) 方法
移除指定索引处的元素。
public E remove(int index) {
// 检查索引是否越界
rangeCheck(index);
// 修改次数+1
modCount++;
// 保存此位置的元素,作为方法返回值
E oldValue = elementData(index);
// 将指定索引后的所有元素向前移动一位
// 计算需要移动的元素个数,若删除的是最后一个元素,则不需要移动
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
// 将末尾空出的位置置为null,帮助GC回收,同时列表大小减一
elementData[--size] = null;
return oldValue;
}
- 检查索引是否越界。
- 修改次数(modCount)+1。
- 保存旧元素,作为方法返回值。
- 若索引不是列表末尾,则需要将此索引之后的所有元素向前移动一位。
- 将列表末尾置为null,帮助GC回收内存,同时列表大小减一。
remove(Object o) 方法
移除指定元素。
public boolean remove(Object o) {
// 对于null和非null元素做了区分,防止触发NPE
// 从头开始遍历,移除第一个匹配的元素
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;
}
private void fastRemove(int index) {
// 修改次数+1
modCount++;
// 将指定位置后的所有元素向前移动一位
// 计算需要移动的元素个数,若删除的是最后一个元素,则不需要移动
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
// 将末尾空出的位置置为null,帮助GC回收,同时列表大小减一
elementData[--size] = null;
}
- 判断传入元素是否为null,为null使用 == 进行比对,不为null使用equals比较。
- 从下标0开始遍历元素数组elementData,对第一个匹配的元素进行删除。
- 修改次数+1。
- 若被删除元素索引不在列表末尾,则需要将此索引之后的所有元素向前移动一位。
- 将末尾空出的位置置为null,帮助GC回收内存,同时列表大小减一。
removeAll(Collection<?> c) 方法
删除给定集合中存在的元素,即求两个集合的差集。
public boolean removeAll(Collection<?> c) {
// 判空
Objects.requireNonNull(c);
return batchRemove(c, false);
}
/**
* 批量删除
* complement为true代表删除c中不存在的元素
* complement为false代表删除c中存在的元素
*/
private boolean batchRemove(Collection<?> c, boolean complement) {
final Object[] elementData = this.elementData;
// r为读指针,遍历整个elementData
// w为写指针,只有在满足条件写入时才+1
int r = 0, w = 0;
boolean modified = false;
try {
for (; r < size; r++)
// 根据c中是否包含此元素以及complement参数的值决定是否保留此元素
if (c.contains(elementData[r]) == complement)
elementData[w++] = elementData[r];
} finally {
// 如果c.contains抛出异常,则r可能不等于size
// 此时需要将elementData中所有还未读取的元素复制到写指针后
if (r != size) {
System.arraycopy(elementData, r,
elementData, w,
size - r);
w += size - r;
}
if (w != size) {
// 将写指针后的元素置为null,帮助GC回收
for (int i = w; i < size; i++)
elementData[i] = null;
// 修改次数+(size - w) (size - w 为删除的元素个数)
modCount += size - w;
// 修改列表大小
size = w;
modified = true;
}
}
return modified;
}
- 遍历整个elementData,将c中存在的元素向前移动,覆盖掉c中不存在的元素,遍历完成后,写指针w前的元素为两个集合都存在的元素。
- 将写指针w之后的元素置为null,帮助GC回收内存。
- 修改列表大小。
removeIf(Predicate<? super E> filter) 方法
根据条件移除元素。
public boolean removeIf(Predicate<? super E> filter) {
Objects.requireNonNull(filter);
// 标记所有要被移除的元素
// 在此阶段所有filter引发的异常将会使集合保持不变
int removeCount = 0;
// 标记需要删除元素的位图
final BitSet removeSet = new BitSet(size);
// 保存当前modCount值
final int expectedModCount = modCount;
final int size = this.size;
// 开始标记阶段
// 对所有元素进行遍历,同时会对modCount进行检查,若有其他线程对集合进行了修改,则抛出异常
for (int i=0; modCount == expectedModCount && i < size; i++) {
@SuppressWarnings("unchecked")
final E element = (E) elementData[i];
// 调用filter进行判断
if (filter.test(element)) {
// 如果此元素需要删除,则将位图此索引处的位置为true
removeSet.set(i);
removeCount++;
}
}
// 若在标记期间集合被修改,抛出异常
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
// 开始删除阶段
final boolean anyToRemove = removeCount > 0;
if (anyToRemove) {
// 移除符合条件元素后的新大小
final int newSize = size - removeCount;
// 保留elementData中的所有非删除元素
// 指针i遍历所有非删除元素的下标
// 指针j每次递增
for (int i=0, j=0; (i < size) && (j < newSize); i++, j++) {
// 取得位图中从索引i开始,第一个设置为false的位的索引,即取得
// 从i开始,第一个非删除元素的索引
i = removeSet.nextClearBit(i);
elementData[j] = elementData[i];
}
// 将空出的位置置为null,帮助GC回收
for (int k=newSize; k < size; k++) {
elementData[k] = null;
}
this.size = newSize;
// 若此期间有其他线程修改集合,则抛出异常
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
modCount++;
}
return anyToRemove;
}
- 标记阶段
- 对所有元素进行遍历,同时会对modCount进行检查,若有其他线程对集合进行了修改,则抛出异常。
- 如果此元素需要删除,则将位图此索引处的位设置为true。
- 删除阶段
- 计算删除后的新大小newSize。
- 根据标记位图将elementData中所有非删除元素向前移动,覆盖标记了删除的元素。
- 从newSize开始,将elementData后面的所有位置设置为null,帮助GC回收内存。
- 若此操作期间有其他线程修改集合,则抛出异常。
- 修改次数+1。
retainAll(Collection<?> c) 方法
保留指定集合中存在的元素,删除其他元素,即求两个集合的交集。
public boolean retainAll(Collection<?> c) {
// 判空
Objects.requireNonNull(c);
// 批量删除,与removeAll(Collection<?> c)方法类似,只是这里是删除所有不在c中的元素
return batchRemove(c, true);
}
trimToSize()方法
对列表进行缩容。
public void trimToSize() {
// 修改次数+1
modCount++;
// 如果元素个数小于数组大小,对elementData进行缩容
// 如果size为0,则将elementData置为EMPTY_ELEMENTDATA,
// 否则将elementData的大小调整为size
if (size < elementData.length) {
elementData = (size == 0)
? EMPTY_ELEMENTDATA
: Arrays.copyOf(elementData, size);
}
}
- 若列表大小为0,则将elementData赋值为EMPTY_ELEMENTDATA。
- 否则将elementData的大小调整为size。
总结
- ArrayList底层数据结构为数组。
- ArrayList默认初始化容量为10。可以通过调用ArrayList(int initialCapacity)构造函数传入指定的初始容量,当已知要使用的大概容量时,使用此构造函数可以节省ArrayList频繁扩容的开支。
- ArrayList在容量不够时会进行自动扩容,容量增加为原来的1.5倍。
- ArrayList不会自动缩容,但是可以通过调用trimToSize()方法进行手动缩容。
- ArrayList查询和访问效率较高,支持快速随机访问。
- ArrayList在列表尾部插入、删除元素时效率较高,但是被操作元素不在列表尾部时插入、删除效率较低,因为涉及到数组元素的迁移。
- 综上,ArrayList适合用于查询大于修改的场景。
- ArrayList非线程安全,多线程环境下不建议使用。