写在开头
本文作为我学习 Java 集合 ArrayList 的一个记录与总结,后续会陆续总结 JAVA 中常用的集合。
ArrayList
一、ArrayList 简介
1.1 ArrayList 概述
- ArrayList 是可以动态增长或缩减的索引序列,它的底层实现是基于数组的
- ArrayList 类内部维护了一个 Object[] 数组来存储元素,并维护了 Capacity 属性来表示 Object[] 数组的长度,当向 ArrayList 中添加元素时会增加 Capacity 的值
- 如果需要向 ArrayList 中添加大量元素时可以先调用 ensureCapacity() 来扩容 Object[] 数组,然后再添加元素以减少扩容中的数组复制。
- ArrayList 与 Vector 相似,但 Vector 性能不佳且非常古老不建议使用
ArrayList 与 Vector 主要区别在于 ArrayList 是线程不安全的,而 Vector 是线程安全的,多个线程同时访问 ArrayList 时需要手动维护数据的准确性,但是即使需要线程的安全的场景也不建议使用 Vector 其内部方法大量使用 synchronized 关键字导致性能降低,应该使用 CopyOnWriteArrayList。 - 继承结构:
1.2 ArrayLsit 底层数据结构
ArrayList 的底层数据结构为数组,其存放 Object 类型的元素即可以存放所有类型数据。
因此所有对 ArrayList 的操作都是基于数组的。
二、ArrayList 源码分析
2.1 继承类与接口实现
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, Serializable
- 继承类:
ArrayList extends AbstractList
AbstractList extends AbstractCollection
其中 AbstractList 也实现了 List 接口,其主要目的是先在 AbstractList 类中实现一些 List 这种集合的通用方法以达到减少重复代码量的目的。
- 实现接口:
- List 接口:在 AbstractList 中已经实现了 List 接口,而在 ArrayList 再次实现只是因为一个小小的mistake,详情可查看 StackOverflow
- RandomAccess 接口:标记接口,查阅 API 文档其作用为快速随机存取,根据 API 文档的描述实现了此接口后使用 for 循环来实现遍历效率更高,见下图:
- Cloneable 接口:实现了这个接口就可以使用 clone() 方法
- Serializable 接口:实现了这个接口表明该类是可序列化的
2.2 成员变量
// 版本号
private static final long serialVersionUID = 8683452581122892189L;
// 默认的 Object[] 数组大小
private static final int DEFAULT_CAPACITY = 10;
// 空数组对象
private static final Object[] EMPTY_ELEMENTDATA = {};
// 默认空对象数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// ArrayList 存储数据元素的数组
transient Object[] elementData;
// ArrayList 的 size
private int size;
2.3 构造方法
- ArrayList(int):
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);
}
}
此构造方法,初始化一个长度为 initialCapacity 的 Object[] 数组
- ArrayList():
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
初始化一个空的 Object[] 数组
- ArrayList(Collection<? extends E> c):
//这个构造方法不常用,举个例子就能明白什么意思
/*
Strudent exends Person
ArrayList<Person>、 Person这里就是泛型
我还有一个Collection<Student>、由于这个Student继承了Person,那么根据这个构造方法,我就可以把这个Collection<Student>转换为ArrayList<Sudent>这就是这个构造方法的作用
*/
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray(); //转换为数组
size = elementData.length; //数组中的数据个数
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class) //每个集合的toarray()的实现方法不一样,所以需要判断一下,如果不是Object[].class类型,那么久需要使用ArrayList中的方法去改造一下。
elementData = Arrays.copyOf(elementData, size, Object[].class);
}
2.4 核心方法
ArrayList 的核心方法有 add(), get(), set(), remove(), indexOf()
2.4.1 add() 方法
- add(Object)
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
// 可以从这里看到 ArrayList 在增加元素是将元素赋值给数组对应位置
elementData[size++] = e;
return true;
}
从代码中可以看到向 ArrayList 中增加元素时就是在将数组对应索引位置赋值,而在赋值之前会调用 ensureCapacityInternal() 方法其作用是动态的扩容数组,接下来就看看这个方法究竟做了哪些事情。
- ensureCapacityInternal() 方法
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
- calculateCapacity() 方法
private static int calculateCapacity(Object[] elementData, int minCapacity) {
// 判断传入的数组是否是空数组
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
// 如果是空数组则返回默认的 DEFAULT_CAPACITY 其值10
// 也就是说当对空数组第一次调用 add() 方法 Object[] 数组的长度会被扩充到10
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
- ensureExplicitCapacity() 方法
private void ensureExplicitCapacity(int minCapacity) {
// modCount 表示 ArrayList 被增加或删除元素的次数
modCount++;
// overflow-conscious code
// 判断是否要扩容
if (minCapacity - elementData.length > 0)
// 调用 ArrayList 能动态增长的核心方法 grow()
grow(minCapacity) ;
}
首先 ensureCapacityInternal() 调用 calculateCapacity() 计算 minCapacity 若 ArrayList 为空即底层的 Object[] 是空数组,那么minCapacity 会被设置为默认的 Capacity 也就是 10, 若不为空则会 return size+1, 接着 ensureExplicitCapacity() 会首先将 modCount 加 1,其是定义在父类 AbstractList 中的成员变量,主要作用是防止在读取 ArrayList 的过程中对其进行修改,下文会具体叙述其作用。然后判断是否需要扩容数组,若需要扩容那么就调用 ArrayList 能够动态增长的核心方法 grow()。
- grow() 方法
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
// 将新的 Capacity 扩容为原来的 1.5 倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8
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);
}
从 grow() 方法中可以看到,其将数组的新长度扩容为旧长度的 1.5 倍并与之前计算出的 minCapacity 比较选择较大的那个作为 Object[] 数组的新长度,最后使用 Arrays.copyOf
通过复制来完成 ArrayList 的扩容,正是由于这种方式 ArrayList 不适合频繁的修改因为复制数组的开销较大,在其他核心方法中也是通过数组的复制来完成,但也正是因为其底层是通过数组实现 ArrayList 的读取效率较高。
- add(int, Object)
/**
在指定 index 出插入一个元素
**/
public void add(int index, E element) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // Increments modCount!!
// 将 index及右边元素向右移动一位
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
- addAll(Collection<> c)
// 此方法将一个集合的所有元素添加到 ArrayList 的末尾,同样是通过 System.arraycopy() 来完成元 素的添加。
public boolean addAll(Collection<? extends E> c) {
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew); // Increments modCount
System.arraycopy(a, 0, elementData, size, numNew);
size += numNew;
return numNew != 0;
}
- addAll(int, Collection<> c)
//此方法将一个集合的所有元素添加到 ArrayList 指定的 index 处,同样是通过 System.arraycopy() 来完成元素的添加。
public boolean addAll(int index, Collection<? extends E> c) {
rangeCheckForAdd(index);
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew); // Increments modCount
int numMoved = size - index;
if (numMoved > 0)
System.arraycopy(elementData, index, elementData, index + numNew,
numMoved);
System.arraycopy(a, 0, elementData, index, numNew);
size += numNew;
return numNew != 0;
}
总结:
- ArrayList 通过 grow() 来动态增长其容量,每次增长为原来的 1.5 倍
- ArrayList 的最大容量为 MAX_VALUE -8
- ArrayList 通过数组复制的方式来动态的增长
2.4.2 get() 方法
public E get(int index) {
// 判断输入的 index >= size
rangeCheck(index);
return elementData(index);
}
elementData() 方法
E elementData(int index) {
//可以看到 get() 方法实际上就是返回底层 Object[] 数组 index 处的元素,这里还通过 (E) 将 元素从 Object--> E 进行向下转型。
return (E) elementData[index];
}
2.4.3 set() 方法
public E set(int index, E element) {
rangeCheck(index);
E oldValue = elementData(index);
//set() 方法将 index 处的赋值为新元素,并返回旧元素的值。
elementData[index] = element;
return oldValue;
}
2.4.4 remove() 方法
与 remove() 相关的方法有很多,其中 fastRemove() 被 两个 remove(Object) 调用,batchRemove() 被 removeAll() 和 retainAll() 调用,removeIf 是 java8 中 Collection 的新特性其能够按照一定的条件筛选集合中的元素。
- remove(int)
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
// 将数组元素从 index + 1 位置整体左移一位
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
// 将空出的位置值为 null 让 gc 更快的回收
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
- remove(Object)
public boolean remove(Object o) {
// 判断传入的是否为 null 从这里可以看出 ArrayList 可以存放 null
if (o == null) {
// 找到第一个 index 并调用 fastRemove
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
// 找到第一个 index 并调用 fastRemove
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
- fastRemove(int)
private void fastRemove(int index) {
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
// 通过数组复制来覆盖 index 处的元素
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
// 将多出的元素至为 null 让 gc 更快的回收
elementData[--size] = null; // clear to let GC do its work
}
- clear()
public void clear() {
modCount++;
// 直接将 Object[] 中元素全部置为 null
// clear to let GC do its work
for (int i = 0; i < size; i++)
elementData[i] = null;
size = 0;
}
- removeAll(Collection<> c)
// 此方法删除 ArrayList 与集合 c 的交集
public boolean removeAll(Collection<?> c) {
// 要求 c 要非空
Objects.requireNonNull(c);
return batchRemove(c, false);
}
- retainAll(Collection<> c)
// 此方保留 ArrayList 与集合 c 的交集
public boolean retainAll(Collection<?> c) {
// 要求 c 要非空
Objects.requireNonNull(c);
return batchRemove(c, true);
}
- batchRemove(Collection<> c, boolean)
private boolean batchRemove(Collection<?> c, boolean complement) {
final Object[] elementData = this.elementData;
int r = 0, w = 0;
boolean modified = false;
try {
for (; r < size; r++)
// 判断是否需要数组元素包含在集合中
// removeAll 为 false
// retainall 为 ture
if (c.contains(elementData[r]) == complement)
elementData[w++] = elementData[r];
} finally {
// Preserve behavioral compatibility with AbstractCollection,
// even if c.contains() throws.
// 如果 r 没有遍历完整个数字那么将剩余的元素复制到[w, w+size-r]
if (r != size) {
System.arraycopy(elementData, r,
elementData, w,
size - r);
w += size - r;
}
if (w != size) {
// clear to let GC do its work
// 将 index = w 之后的元素全部置为 null
for (int i = w; i < size; i++)
elementData[i] = null;
modCount += size - w;
size = w;
modified = true;
}
}
return modified;
}
- removeIf(Predicate<>)
removeIf 是 java8 中 Collection 新添加的方法,其能够通过规定一个漏斗 filter 来过滤掉符合条件的元素。
public boolean removeIf(Predicate<? super E> filter) {
Objects.requireNonNull(filter);
// figure out which elements are to be removed
// any exception thrown from the filter predicate at this stage
// will leave the collection unmodified
int removeCount = 0;
// 创建一个 BitSet,BitSet能够通过 set(index) 方法将 index位置置为1,其余位置
// 则为 0,例如:
// 原始 BitSet为 0000000
// 调用 set(2)
// 新的 BitSet 为 0010000
final BitSet removeSet = new BitSet(size);
final int expectedModCount = modCount;
final int size = this.size;
for (int i=0; modCount == expectedModCount && i < size; i++) {
@SuppressWarnings("unchecked")
final E element = (E) elementData[i];
// 遍历数组找出符合过滤条件的元素并在 BitSet 中置为 1
if (filter.test(element)) {
removeSet.set(i);
removeCount++;
}
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
// shift surviving elements left over the spaces left by removed elements
final boolean anyToRemove = removeCount > 0;
if (anyToRemove) {
final int newSize = size - removeCount;
for (int i=0, j=0; (i < size) && (j < newSize); i++, j++) {
// BitSet 的 nextClearBit 找到第一个 0 位置,即找出所有不需要过滤的元素
i = removeSet.nextClearBit(i);
elementData[j] = elementData[i];
}
for (int k=newSize; k < size; k++) {
// 将空余的位置置为 null 让 gc 更快回收
elementData[k] = null; // Let gc do its work
}
this.size = newSize;
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
modCount++;
}
return anyToRemove;
}
总结:
- remove() 方法就是将满足条件的第一个元素通过复制元素并覆盖的方法删除
- removeAll() 和 retainAll() 方法相反,前者删除与集合 c 的交集,后者保留与集合 c 的交集
- removeIf() 方法通过设置一个过滤器来删除那些满足过滤器条件的元素
- clear() 方法直接将整个数组置为 null
- 在所有删除相关方法执行的最后都会将多余的元素置为 null 让 gc 更快的回收
2.4.5 indexOf() 方法
public int indexOf(Object o) {
// 判断输入元素是否为 null
if (o == null) {
// 返回第一个 null 元素
for (int i = 0; i < size; i++)
if (elementData[i]==null)
return i;
} else {
// 返回第一个相等的非 null 元素
for (int i = 0; i < size; i++)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
三、ArrayList 并发修改异常
3.1 简单案例
都说 ArrayList 是线程不安全在并发读写的时候会出错,接下来就用一个简单的案例来模拟一下这个过程,然后分析源码探寻其原因。
public class ArrayTest {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("aaa");
list.add("bbb");
list.add("ccc");
list.add("ddd");
// 获得迭代器
Iterator<String> iterator = list.iterator();
while(iterator.hasNext()){
String next = iterator.next();
// 在迭代器读取 ArrayList 时对其进行修改
if(next.equals("bbb")){
list.remove(next);
}
}
}
}
运行上面这段代码会报 java.util.ConcurrentModificationException 异常,这就是导致不能并发写的原因。
3.2 源码分析
接下来我们分析一下源码看看是如何导致的这个异常
// 首先我们通过 iterator() 方法获得迭代器
// ArrayList 中源码如下
// 其返回了一个 Itr 对象
public Iterator<E> iterator() {
return new Itr();
}
- Itr 类
我们主要看其成员变量和导致错误的方法
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
// 这里的 expectedModCount 就是导致异常的关键
int expectedModCount = modCount;
Itr() {}
final void checkForComodification() {
// 当全局的 modCount 和 迭代器对象的 expectedModCount 不一致时就会报错
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
从上面的代码可以看到在我们初始化迭代器对象时其内部会初始化一个成员变量 expectedModCount 其值等于 modCount,然后在我们执行 remove() 时 modCount 会被修改,具体看代码
public boolean remove(Object o) {
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) {
// 此处修改了 modCount 导致与 expectedModCount 不相等
modCount++;
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
}
可以看到在调用 remove() 方法时会让 modCount 增加,前面说过这个变量就是用来记录 ArrayList 增加或删除的次数,而增加后就与 Itr 对象中的 expectedModCount 不相同从而导致错误,因此在使用迭代器读取 ArrayList 时不能对其进行修改。
解决方法:
若要在迭代时删除元素可以使用 Itr 对象提供的 remove() 方法
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("aaa");
list.add("bbb");
list.add("ccc");
list.add("ddd");
Iterator<String> iterator = list.iterator();
while(iterator.hasNext()){
String next = iterator.next();
if(next.equals("bbb")){
iterator.remove();
}
}
System.out.println(list);
}
可以看到使用 Itr 对象提供的 remove() 方法删除元素不会导致异常观察源码其在删除元素后将 expectedModCount 与 全局的 modCount 进行了同步,因此不会导致异常。
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
// 将 expectedModCount 与 modCount 进行同步
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
以上方法只能在单线程下避免读取 ArrayList 时删除导致的异常,多个线程下依然会报错,为了解决此问题可以使用 JAVA 的并发 List 容器 CopyOnWriteArrayList,对于其的介绍就留给其他文章详细说明。
以上方法只能在单线程下避免读取 ArrayList 时删除导致的异常,多个线程下依然会报错,为了解决此问题可以使用 JAVA 的并发 List 容器 CopyOnWriteArrayList,对于其的介绍就留给其他文章详细说明。
四、总结
- ArrayList 本质是 Object[] 数组
- ArrayList 能够实现动态增长其核心是 grow() 方法,此方法每次将 ArrayList 扩容为原来的 1.5 倍
- ArrayList 的底层是数组因此其修改的效率不高需要通过数组复制或者移动大量元素来达到目的,但其查询效率较高
- ArrayList 线程不安全,在读取时修改 ArrayList 会导致异常,单线程情况下可以通过迭代器的 remove() 方法完成删除,多线程情况下应使用 JAVA 并发容器 CopyOnWriteArrayList
- ArrayList 实现了 RandomAccess 接口使用 for 循环遍历效率更高