文章目录
简介
- ArrayList继承自 AbstractList,实现了 List 接口。底层基于数组实现容量大小动态变化。允许 null 的存在。同时还实现了 RandomAccess、Cloneable、Serializable 接口,所以ArrayList 是支持快速访问、复制、序列化的。
ArrayList的常量与变量
- ArrayList 底层是基于数组来实现容量大小动态变化的。
// 存放当前数据,不参与序列化
transient Object[] elementData; // non-private to simplify nested class access
// 元素个数大小
private int size;
上面的 size 是指 elementData 中实际有多少个元素,而 elementData.length 为集合容量,表示最多可以容纳多少个元素。
- 默认初始容量大小为 10
// 序列ID
private static final long serialVersionUID = 8683452581122892189L;
// ArrayList默认的初始容量大小
private static final int DEFAULT_CAPACITY = 10;
- 两个空数组,有什么区别呢?
// 空对象数组,用于空实例的共享空数组实例
private static final Object[] EMPTY_ELEMENTDATA = {};
// 空对象数组,如果使用默认的构造函数创建,则默认对象内容是该值
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
简单来说: 调用无参构造是用Default,调用有参构造时如果初始长度为0则用Empty
当集合中的元素超出数组规定的长度时,数组就会进行扩容操作,扩容操作就是ArrayList存储操作缓慢的原因,尤其是当数据量较大的时候,每次扩容消耗的时间会越来越多
ArrayList的构造方法
ArrayList()
/**
* Constructs an empty list with an initial capacity of ten.
*/
// 默认的构造方法,构造一个初始容量为10的空列表
public ArrayList() {
// elementData 初始化为 DEFAULTCAPACITY_EMPTY_ELEMENTDATA
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
注意:注释是说构造一个容量大小为 10 的空的 list 集合,但构造函数只是给 elementData 赋值了一个空的数组,其实是在第一次添加元素时容量扩大至 10 的,在grow源码里面里面
ArrayList(int initialCapacity)
- 所以当我们要使用ArrayList时,可以 new ArrayList(大小)构造方法来指定集合的大小,以减少扩容的次数,提高写入效率,该构造函数的源码如下:
// 自定义初始容量的构造方法
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
// 如果初始容量小于0,则会出现 IllegalArgumentException 异常
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
ArrayList(Collection c)
- 将 Collection 转化为数组并赋值给 elementData,把 elementData 中元素的个数赋值给 size。 如果 size 不为零,则判断 elementData 的 class 类型是否为 Object[],不是的话则做一次转换。 如果 size 为零,则把 EMPTY_ELEMENTDATA 赋值给 elementData,相当于new ArrayList(0)。
// 构造一个包含指定元素的列表集合,按集合的返回顺序迭代器
// 传入参数为Collection对象
// c要将其元素放入此列表的集合
public ArrayList(Collection<? extends E> c) {
// 调用toArray()方法将Collection对象转换为Object[]
elementData = c.toArray();
// 判断size的大小,如果size值为0,则会抛出NullPointerException异常
// 如果size > 0 ,则执行以下代码
if ((size = elementData.length) != 0) {
//判断 elementData 的 class 类型是否为 Object[],不是的话则做一次转换
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// 相当于new ArrayList(0)
this.elementData = EMPTY_ELEMENTDATA;
}
}
ArrayList的方法
add()
- 首先会判断是否需要扩容
//参数 :需要添加的元素,需要添加的数组,数组元素的个数
private void add(E e, Object[] elementData, int s) {
//如果数组长度等于元素个数则需要扩容
if (s == elementData.length)
elementData = grow();
//否则直接赋值
elementData[s] = e;
size = s + 1;
}
public boolean add(E e) {
//将 modCount 增加一(迭代时,将 expectedModCount 设为 modCount,若迭代过程中对集合进行了操作,易知
//expectedModCount 与 modCount 数值不等,则会抛出 ConcurrentModificationException 异常)
//这个参数是指当前列表的结构被修改的次数
modCount++;
add(e, elementData, size);
return true;
}
- 扩容方法
grow
一般情况的扩容是1.5倍,如果期望长度大于1.5倍扩容长度 则直接用期望长度进行扩容
//参数:数组元素个数加一
private Object[] grow(int minCapacity) {
//原来的数组长度
int oldCapacity = elementData.length;
//如果原来数组为空,或者初始化为new ArrayList(0) 则直接返回一个大小为10的默认数组
//这也就是上面说的第一次添加元素时容量扩大至 10
if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
//扩容调用ArraysSupport.newLength方法 将 原来数组长度,元素个数加一 减 数组长度(也就是1),原来数组长度的一半。传入
int newCapacity = ArraysSupport.newLength(oldCapacity,
minCapacity - oldCapacity, /* minimum growth */
//异或运算 减半
oldCapacity >> 1 /* preferred growth */);
//扩容后的长度 和原来的元素 复制给elementData
return elementData = Arrays.copyOf(elementData, newCapacity);
} else {
return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
}
}
private Object[] grow() {
return grow(size + 1);
}
ArraysSupport.newLength()
扩容
//参数:原来数组长度,元素个数加一-数组长度(也就是1),原来数组长度的一半
public static int newLength(int oldLength, int minGrowth, int prefGrowth) {
//如果原数组长度一半小于了期望的长度 则直接用期望的长度 比如 原数组长度为1 期望1 一半为0.5 所以扩容为 2
int newLength = Math.max(minGrowth, prefGrowth) + oldLength;
如果新数组的长度小于可分配数组的最大值 直接返回
if (newLength - MAX_ARRAY_LENGTH <= 0) {
return newLength;
}
//否则调用hugeLength 把原来数组长度和1传入
return hugeLength(oldLength, minGrowth);
}
private static int hugeLength(int oldLength, int minGrowth) {
int minLength = oldLength + minGrowth;
if (minLength < 0) { // overflow
throw new OutOfMemoryError("Required array length too large");
}
if (minLength <= MAX_ARRAY_LENGTH) {
return MAX_ARRAY_LENGTH;
}
//将扩容长度设置为Interger.MAX_VALUE,也就是int的最大长度 如果超出则报堆内存异常
return Integer.MAX_VALUE;
}
总结:
-
在进行 add 操作时先判断下标是否越界,是否需要扩容,如果需要扩容,就复制数组,然后设置对应的下标元素值
-
扩容:默认扩容一半,如果扩容一半不够的话,就用目标的size作为扩容后的容量
get()
- 先判断索引越界,再寻值
// 先判断下标索引
public E get(int index) {
// 调用rangeCheck判断是否超出了Object数组长度
Objects.checkIndex(index, size);
// 调用 elementData 方法
return elementData(index);
}
rangeCheck
判断是否超出了Object数组长度 底层就是一个if语句
elementData(index)
方法
// 通过下标索引找到对应的元素值,返回指定元素
E elementData(int index) {
return (E) elementData[index];
}
set()
- 首先也是判断是否越界
public E set(int index, E element) {
// 调用rangeCheck判断是否超出范围
Objects.checkIndex(index, size);
// 返回指定元素,上面也讲到过
E oldValue = elementData(index);
elementData[index] = element;
//返回修改前的值
return oldValue;
}
set方法返回的是原来的值oldValue
remove()
- 通过底层数组索引删除
public E remove(int index) {
//越界
Objects.checkIndex(index, size);
final Object[] es = elementData;
@SuppressWarnings("unchecked") E oldValue = (E) es[index];
//将需要删除的后面的元素全部向前移动一个元素位置,然后将最后一个元素(等于前一个元素)赋值为 null
fastRemove(es, index);
//返回删除前的值
return oldValue;
}
fastRemove
private void fastRemove(Object[] es, int i) {
//这个参数是指当前列表的结构被修改的次数
modCount++;
final int newSize;
if ((newSize = size - 1) > i)
// 通过 System.arraycopy 方法将删除后面的元素往前移动一位
System.arraycopy(es, i + 1, es, i, newSize - i);
es[size = newSize] = null;
}
- 通过元素值删除
public boolean remove(Object o) {
final Object[] es = elementData;
final int size = this.size;
int i = 0;
//循环遍历找到要删除的值
found: {
//删除的值为空
if (o == null) {
for (; i < size; i++)
if (es[i] == null)
break found;
} else {
for (; i < size; i++)
if (o.equals(es[i]))
break found;
}
//说明没有要删除的值
return false;
}
//同理
fastRemove(es, i);
return true;
}
ArrayList线程不安全
- 并发环境下进行add操作时可能会导致elementData数组越界
问题现场如下:
有两个线程:t1,t2。有ArrayList size=9(即其中有9个元素)。elementData.length=10
t1进入add()方法,这时获取到size值为9,判断容量是否需要扩容
t2也进入add()方法,这时获取到size值也为9,判断容量是否需要扩容
t1发现自己的需求为size!=10,容量足够,无需扩容
t1发现自己的需求为也size!=10,容量足够,无需扩容
t1开始设置元素操作,elementData[s] = e;size = s + 1;,成功,此时size变为10
t2也开始进行设置元素操作,它尝试设置elementData[10] = e,而elementData没有进行过扩容,它的下标最大为9。于是此时会报出一个数组越界的异常:ArrayIndexOutOfBoundsException
-
一个线程的值覆盖另一个线程添加的值
- size大小符合预期,但是中间有null值存在
问题现场如下: 有两个线程:t1,t2。有ArrayList size=5(即其中有5个元素)。elementData.length=10 t1进入add()方法,这时获取到size值为5,判断容量是否需要扩容 t2也进入add()方法,这时获取到size值也为5,判断容量是否需要扩容 t1发现自己的需求为size!=6,容量足够,无需扩容 t1发现自己的需求为也size!=6,容量足够,无需扩容 t1开始设置元素操作,elementData[s] = e,成功, t2也开始设置元素操作,elementData[s] = e,成功,注意此时t1的size = s + 1还没执行 t1 size = size + 1 = 6,并写入主存 t2 size = size + 1 = 7 这样,size符合预期,但是t2设置的值被覆盖,而且索引为6的位置将永远为null,因为size已经为7,下次add()也会从7开始。除非手动set值。
- size大小比预期的小
问题现场如下: 有两个线程:t1,t2。有ArrayList size=5(即其中有5个元素)。elementData.length=10 t1进入add()方法,这时获取到size值为5,判断容量是否需要扩容 t2也进入add()方法,这时获取到size值也为5,判断容量是否需要扩容 t1发现自己的需求为size+1=6,容量足够,无需扩容 t1发现自己的需求为也size+1=6,容量足够,无需扩容 t1开始设置元素操作,elementData[s] = e,成功, t2也开始设置元素操作,elementData[s] = e,成功,注意此时t1的size = s + 1还没执行//也会有null t1 size = size + 1 = 6,暂未写入主存 t2 size = size + 1 此时因为t1操作完size还未写入主存,所以size依然为5,+1后仍为6 t1将size=6 写入主存 t2将size=6 写入主存 这样,size=6 比预期结果小了。