ArrayList
ArrayList
概述
ArrayList 我们几乎每天都会使用到,在接口中返回一个列表,通常使用 ArrayList 装载对象返回。可以说是开发中最为常用的数据结构工具。ArrayList 允许 put null 值,会自动扩容,可以简化很多工作,ArrayList 为非线程安全,适合作为函数局部变量使用。从整体上看 ArrayList 比较简单,底层使用数组结构实现。ArrayList 具有以下几个基本概念:
- DEFAULT_CAPACITY 表示数组的初始大小,默认是 10;
- size 表示当前数组的大小,类型 int,没有使用 volatile 修饰,非线程安全的;
- elementData 数据存储的地方,是一个数组,数据的操作都围绕该属性进行;
- DEFAULTCAPACITY_EMPTY_ELEMENTDATA 是一个数组,空构造使用该数组进行初始化;
- modCount 统计当前数组被修改的版本次数,数组结构有变动,就会 +1;
//DEFAULT_CAPACITY 表示数组的初始大小,默认是 10 在扩容的时候会用到
private static final int DEFAULT_CAPACITY = 10;
private static final Object[] EMPTY_ELEMENTDATA = {};
// DEFAULT_CAPACITY 表示数组的初始大小,默认是 10 在扩容的时候会用到
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 数据存储的地方,使用数组存储,数据的操作都围绕该属性进行
transient Object[] elementData; // non-private to simplify nested class access
// 数组的大小
private int size;
核心操作
初始化
无参数直接初始化
public ArrayList() {
// 直接使用 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 空数组构造,这是我们常见的写法
// 注意这种写法默认数组的长度是为 0 ,并非 10,每次添加都需要扩容,所以工作中不太建议这么写
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
指定大小初始化
public ArrayList(int initialCapacity) {
// 指定长度为 initialCapacity, 根据长度进行初始化
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
// 这里为什么不用 DEFAULTCAPACITY_EMPTY_ELEMENTDATA?
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity);
}
}
指定初始数据初始化
public ArrayList(Collection<? extends E> c) {
//elementData 是保存数组的容器,默认为 null
elementData = c.toArray();
//如果给定的集合(c)数据有值
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
//如果集合元素类型不是 Object 类型,我们会转成 Object
if (elementData.getClass() != Object[].class) {
elementData = Arrays.copyOf(elementData, size, Object[].class);
}
} else {
// 给定集合(c)无值,则默认空数组
this.elementData = EMPTY_ELEMENTDATA;
}
}
ArrayList 提供了三种构造方法:
- 使用无参构造器初始化时,使用 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 初始化 elementData,此时空间大小(数组的长度)是 0;
- 指定大小 initialCapacity 初始化,将使用该大小来初始化 elementData,此时空间大小(数组的长度)是 initialCapacity;
- 指定初始数据初始化,将给定的初始数据转化为数组,使用 Arrays.copyOf 拷贝到 elementData;
添加元素
public boolean add(E e) {
// 确保内部空间容量
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
这里很简单,首先确保空间容量,对数组的空间大小做一个校验,然后对数组对应下标赋值,最后将 size + 1;所以核心在 ensureCapacityInternal 上;
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
这里也很简单,对 elementData 做了判断,如果 elementData 等于 DEFAULTCAPACITY_EMPTY_ELEMENTDATA,也就是如果使用空参数构造方法初始化的话,比较 DEFAULT_CAPACITY 和 minCapacity,将大值赋给 minCapacity,然后走 ensureExplicitCapacity 函数。
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
从这段代码可以看出,每次修改 elementData 数组,modCount 属性默认 + 1,也就是说如果想知道,ArrayList 被扩容了几次,只要查看 modCount 的大小即可。此外,如果要扩容达到的空间大小大于数组当前的空间大小就会走 grow 函数进行扩容。扩容的核心点在在于:ArrayList 会将当前的空间大小增加原来的一半(oldCapacity >> 1),也就是说如果原来空间大小是 22,经过一次扩容之后将会修改为 33,当然如果扩容的大小仍然小于目标大小,那么就直接使用目标大小。 此外值得注意的是,如果使用空参数构造 ArrayList,那么第一次扩容的大小是 10。
此外我们需要关注 Arrays.copyOf 函数,查看这个函数的底层实现,可以知道该函数调用了 System.arraycopy;
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
@SuppressWarnings("unchecked")
T[] copy = ((Object)newType == (Object)Object[].class)
? (T[]) new Object[newLength]
: (T[]) Array.newInstance(newType.getComponentType(), newLength);
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
而 System.arraycopy 函数是 Native 方法,底层调用 C 函数;说明如下:
public static native void arraycopy(Object src, int srcPos, Object dest, int destPos, int length);
src: 源数组。
srcPos: 源数组中的起始位置。
dest: 目标数组。
destPos: 目标数据中的起始位置。
length: 要复制的数组元素的数量。
删除元素
public E remove(int index) {
// 校验 index >= size ,如果是则抛出异常
rangeCheck(index);
// 记录修改次数
modCount++;
// 获取数值中对应下标的的元素
E oldValue = elementData(index);
// 拷贝数据,用 elementData 数组 index 后一位的所有元素覆盖从 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;
}
这里需要关注 System.arraycopy 函数,从上来的介绍可以知道 arraycopy 函数从 src 数组将 srcPos 为开始位置,长度为 length 的元素拷贝到 以 destPos 为起点的 dest 数组上。总结下来,ArrayList 的 remove 核心操作是拷贝数据,用 elementData 数组 index 后一位的所有元素覆盖从 index 之后的所有元素。然后将 size - 1,最后将数组的末尾元素指向空。
总结
从我们上面新增或删除方法的源码解析,对数组元素的操作,只需要根据数组索引,直接新增和删除,所以时间复杂度是 O (1)。ArrayList 其实就是围绕底层数组结构,各个 API 都是对数组的操作进行封装,让使用者无需感知底层实现,只需关注如何使用即可。