前言
ArrayList是我们开发过程中经常用到的集合类,一般涉及到集合操作的业务都会想到它,在java的集合家族中,知名度最高的集合类非他莫属。但是人无完人,ArrayList也存在利与弊,在开发过程中如何权衡呢?让我们通过源码对ArrayList底层进行一个深入的了解,这样就能在使用过程中根据需求来决定是否使用该集合类。
走近ArrayList
ArrayList基于数组实现,在普通数组基础上增加了自动扩容等功能。实现了List, RandomAccess, Cloneable, java.io.Serializable接口。
- List接口: 封装了集合的一些基本操作,比如存储的元素数量,集合是否为空,是否包含某个元素等。
- RandomAccess接口: 是一个标志接口,表示这个当前实现类支持快速随机访问。在ArrayList中,我们可以通过元素下标快速获取对应元素,这就是快速随机访问。
- java.io.Serializable接口: 说明ArrayList支持序列化,能通过序列化在网络中进行传输。
属性
ArrayList属性主要包括存放元素的Object数组、已存放元素的数量以及从AbstractList继承过来的modCount。
// 序列化ID
private static final long serialVersionUID = 8683452581122892189L;
// 默认初始化容量
private static final int DEFAULT_CAPACITY = 10;
// 用于空实例的共享空数组实例
private static final Object[] EMPTY_ELEMENTDATA = {};
// 用于默认大小的空实例的共享空数组实例,将其与 EMPTY_ELEMENTDATA 区分开来,以了解添加第一个元素时要膨胀多少
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 真正存放元素的数组, transient表明不参与序列化
transient Object[] elementData; // non-private to simplify nested class access
// 数组包含元素的个数
private int size;
构造方法
ArrayList有三个构造方法,分别是创建指定容量大小的 ArrayList(int initialCapacity),创建默认数组的 ArrayList(),传入一个集合的 ArrayList(Collection<? extends E> c)。
创建指定容量大小的构造方法
传入的参数initialCapacity代表初始数组长度,若initialCapacity >= 0,根据initialCapacity 构造对应大小的数组;否则抛出异常。
public ArrayList(int initialCapacity) {
// initialCapacity > 0,根据创建容量为initialCapacity 的数组
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
}
// initialCapacity > 0,将EMPTY_ELEMENTDATA赋值给elementData
else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
}
// initialCapacity < 0,抛出异常
else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
无参构造方法
若不传入参数,直接使用DEFAULTCAPACITY_EMPTY_ELEMENTDATA进行初始化。
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
带集合构造方法
将传入的集合转换成数组,对转换后的数组进行长度判断,若长度不等于0,即大于0,则使用Arrays.copyOf方法转换后的数组内容深拷贝到elementData。若长度为0,直接将空对象EMPTY_ELEMENTDATA地址赋值给elementData。
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray可能不返回Object[],需要判断
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// 若c.toArray不返回Object[],用空数组代替
this.elementData = EMPTY_ELEMENTDATA;
}
}
常用方法
add方法
该方法往数组末尾添加元素,添加前调用ensureCapacityInternal方法确保当前数组大小可以容纳添加的元素。
public boolean add(E e) {
// 确保数组大小足够
ensureCapacityInternal(size + 1);
elementData[size++] = e;
return true;
}
来看下ensureCapacityInternal方法:
private void ensureCapacityInternal(int minCapacity) {
// 如果是通过无参构造方法创建,使用DEFAULT_CAPACITY和minCapacity两者之间最大值
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
// 修改次数加1,用于fail-fast机制
modCount++;
// 如果minCapacity大于elementData数组长度,需要进行扩容
if (minCapacity - elementData.length > 0)
// 进行扩容操作
grow(minCapacity);
}
进入扩容方法grow
private void grow(int minCapacity) {
// oldCapacity为旧容量大小,newCapacity为新容量大小
int oldCapacity = elementData.length;
// oldCapacity >> 1相当于oldCapacity / 2,所以newCapacity的大小是oldCapacity的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 若1.5倍后的新容量小于需要容量,直接将需要的容量值作为新容量
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 判断新容量大小是否超过最大容量限制
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 使用深拷贝赋值
elementData = Arrays.copyOf(elementData, newCapacity);
}
private static int hugeCapacity(int minCapacity) {
// 判断是否溢出
if (minCapacity < 0)
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
小结:
先判断容量是否足够,若足够,则直接添加到数组末尾,不够就先进行扩容再添加。扩容需要注意以下两点:
- 默认扩容一半,如果扩容一半后还不够,就用目标的容量大小作为扩容后的容量。
- 扩容成功需要更新modCount值。
remove方法
我们比较常用的删除方法是根据下标删除,根据指定元素删除。即remove(int index) 和 remove(Object o)。
remove(int index)
根据元素下标删除。
public E remove(int index) {
// 判断传入的下标是否越界
rangeCheck(index);
// 由于数组结构有变化,所以modCount值加1
modCount++;
// 获取传入下标的值,用于返回
E oldValue = elementData(index);
// 计算System.arraycopy方法需要的长度
int numMoved = size - index - 1;
if (numMoved > 0)
// 用index后面的数据覆盖掉前面
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
// 将最后一个元素置空,用于GC回收
elementData[--size] = null;
return oldValue;
}
remove(Object o)
根据元素删除,若有多个相同元素则删除第一个出现的。返回true表示删除成功,false表示删除失败。
public boolean remove(Object o) {
// 传入Object为null需要单独处理
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;
}
小结:
根据下标删除和根据元素删除底层都是数组的复制,相对低效,且要修改modCount的值。
set方法
覆盖指定下标的元素。
public E set(int index, E element) {
// 检查传入的下标是否越界
rangeCheck(index);
// 返回修改前的旧值
E oldValue = elementData(index);
// 将指定下标的元素替换为新的值
elementData[index] = element;
return oldValue;
}
小结:
检查是否越界,获取旧值,将指定下标的值替换为新值,返回旧值。
get方法
获取指定下标的元素。
public E get(int index) {
// 检查传入的下标是否越界
rangeCheck(index);
// 返回对应下标的元素值
return elementData(index);
}
小结:
检查是否越界,返回对应下标的元素值。
总结
ArrayList源码总体来说不算太难,代码逻辑比较容易理解。有一些细节的方法我就没展开分析。希望这篇文章对你有所帮助,有什么问题和建议欢迎随时交流~
参考链接
JavaGuide-ArrayList源码+扩容机制分析
面试必备:ArrayList源码解析(JDK8)
ArrayList源码分析(基于JDK8)