1. 整体架构
ArrayList 整体架构比较简单,就是一个 Object 数组,如图所示。
有几个点需要注意:
- DEFAULT_CAPACITY 表示数组的初始大小,默认是 10,这个数字要记住;
- size 表示当前数组的大小,类型 int,没有使用 volatile 修饰,非线程安全的;
- modCount 统计当前数组被修改的版本次数,数组结构有变动,就会 +1。
2. 源码解析
2.1 初始化
ArrayList 有三个进行初始化的构造函数: ```java private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 无参构造器,返回一个 Object[] 空数组 // 无参构造器初始化时,默认大小是空数组,并不是大家常说的 10,10 是在第一次 add 的时候扩容的数组值。 public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; }
// 有参构造器,返回一个给定初始化大小的 Object[] 数组 public ArrayList(int initialCapacity) { if (initialCapacity > 0) { this.elementData = new Object[initialCapacity]; } else if (initialCapacity == 0) { 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 类型,才开始拷贝,否则不执行 if (elementData.getClass() != Object[].class) { elementData = Arrays.copyOf(elementData, size, Object[].class); } } else { // 给定集合(c)无值,则默认空数组 this.elementData = EMPTY_ELEMENTDATA; } }
<a name="rOJh0"></a>
# 2.2 新增和扩容
**ArrayList 的 add() 方法分为两步:**
1. 判断是否需要扩容,如果需要先进行扩容
1. 直接赋值(不加锁,因此是线程不安全的)
**以下是这两个步骤的相关源码:**
```java
public boolean add(E e) {
// 判断是否需要扩容,如果需要先进行扩容
ensureCapacityInternal(size + 1); // Increments modCount!!
// 直接赋值(不加锁,因此是线程不安全的)
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
// 如果是空数组,就从最小容量和默认容量10之间取最大值
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
// 确保容积足够,若容积不够就扩容
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
// 统计当前数组被修改的版本次数,数组结构有变动,就会 +1
modCount++;
// 如果我们希望的最小容量大于目前数组的长度,那么就扩容
// 也就是 size+1 > elementData.length 的时候进行扩容,数组即将满的时候进行扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
// 将原数组扩大1.5倍,把现有数据拷贝到新的数组里面去
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 如果扩容后的值 < 我们的期望值,扩容后的值就等于我们的期望值
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 如果扩容后数组的大小 > jvm所能分配的数组的最大值,那么扩容后数组的大小就用 Integer 的最大值
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);
}
这里有几个需要注意的点:
- 扩容的规则是将原数组扩大1.5倍,然后把现有数据拷贝到新的数组里面去。
- ArrayList 中的数组的最大值是
Integer.MAX_VALUE
,超过这个值,JVM 就不会给数组分配内存空间了。 - 新增时,并没有对值进行严格的校验,所以 ArrayList 是允许 null 值的。
- 扩容完成之后,赋值是非常简单的,直接往数组上添加元素即可:
elementData[size++] = e
。也正是通过这种简单赋值,没有任何锁控制,所以这里的操作是线程不安全的。
扩容的本质:
上面代码的最后一行是扩容操作:elementData = Arrays.copyOf(elementData, newCapacity);
,这行代码描述的本质是数组之间的拷贝,扩容是会先新建一个符合我们预期容量的新数组,然后把老数组的数据拷贝过去,最后调用的是 System.arraycopy
方法进行拷贝,这是一个 native 方法。
2.3 删除
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 {
// 值不为空,找到第一个和入参相等的删除
for (int index = 0; index < size; index++)
// 这里是根据 equals 来判断值相等的
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
需要注意:
- 新增的时候是没有对 null 进行校验的,所以可以删除 null 值
- 算法实现是通过遍历数组找到要删除的目标,然后进行删除
上面代码已经找到要删除元素的索引了,下面代码是根据索引位置进行元素的删除:
private void fastRemove(int index) {
// 统计当前数组被修改的版本次数,数组结构有变动,就会 +1
modCount++;
// 减 1 的原因,是因为 size 从 1 开始算起,index 从 0 开始算起
// numMoved 删除 index 位置的元素后,需要从 index 后移动多少个元素到前面去
int numMoved = size - index - 1;
if (numMoved > 0)
// 从 index +1 位置开始被拷贝,拷贝的起始位置是 index
System.arraycopy(elementData, index + 1, elementData, index, numMoved);
// 数组最后一个位置赋值为 null,帮助 GC
elementData[--size] = null; // clear to let GC do its work
}
从源码中,我们可以看出,某一个元素被删除后,为了维护数组结构,我们都会把数组后面的元素往前移动。
2.4 迭代器
如果要实现迭代器,那么实现 java.util.Iterator 类就好了,ArrayList 也是这样做的。
// 实现 Iterator 接口
private class Itr implements Iterator<E> {
// 迭代过程中,下一个元素的位置,默认从 0 开始。
int cursor; // index of next element to return
// 新增时表示上一次迭代过程中,索引的位置;删除成功时为 -1
int lastRet = -1; // index of last element returned; -1 if no such
// 迭代过程中期望数组修改版本号
int expectedModCount = modCount;
public boolean hasNext() {
return cursor != size;
}
// 检验能不能继续迭代,并且找到迭代的值,并为下一次迭代做准备(cursor+1)。
@SuppressWarnings("unchecked")
public E next() {
//迭代过程中,判断版本号有无被修改,有被修改,抛 ConcurrentModificationException 异常
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];
}
public void remove() {
// 如果上一次操作时,数组的位置已经小于 0 了,说明数组已经被删除完了
if (lastRet < 0)
throw new IllegalStateException();
//迭代过程中,判断版本号有无被修改,有被修改,抛 ConcurrentModificationException 异常
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
// -1 表示元素已经被删除,这里也防止重复删除
lastRet = -1;
// 删除元素时 modCount 的值已经发生变化,再此赋值给 expectedModCount
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
...
}
2.5 线程安全
需要强调的是,只有当 ArrayList 作为共享变量时,才会有线程安全问题,当 ArrayList 是方法内的局部变量时,是没有线程安全的问题的。
为什么 ArrayList 会有线程安全问题?本质原因是 ArrayList 自身的 elementData、size、modConut 在进行各种操作时,都没有加锁,而且这些变量的类型并非是可见(volatile)的,所以如果多个线程对这些变量进行操作时,可能会有值被覆盖的情况。
类注释中推荐我们使用 SynchronizedList 来保证线程安全,SynchronizedList 是通过在每个方法上面加上锁来实现,虽然实现了线程安全,但是性能大大降低。JDK 还提供了另外一种线程安全的 List,叫做CopyOnWriteArrayList。
3. Arrays.asList使用指南
public static <T> List<T> asList(T... a) {
return new ArrayList<>(a);
}
上面是Arrays.List的方法签名,参数类型是T,T是数组元素的class。asList方法的参数必须是对象或者对象数组,而原生数据类型不是对象——这也正是包装类出现的一个主要原因。如果传入一个原生数据类型数组,asList 的真正得到的参数就不是数组中的元素,而是数组对象本身!此时List 的唯一元素就是这个数组。并且,方法返回的ArrayList并不是 java.util.ArrayList ,而是 java.util.Arrays 的一个内部类。
因此,对于Arrays.asList的使用,新手很容易犯两个错误:
- 将原生数据类型数据的数组作为参数。
- 解决方案:使用包装类数组
- 试图修改 List 的大小。
- 解决方案:创建一个真正的 ArrayList