说到面试,高频的当属 Java 的集合类了,这是完全绕不开的一道大坎,而且里面包含了许多的数据结构。而在我们的平常使用中,ArrayList 几乎可以说是随处可见,尤其是对刚入行的朋友们来说,ArrayList 可以说是万金油了,今天我们就来好好的看看它 里面到底有些啥,平常我们的使用又该怎么注意。
注意,系列文章源码使用 Java 8 !
正文开始
在使用一个类的时候,我们首先是要实例化,那么我们先看 ArrayList 的构造方法。
构造函数:
// 指定初始容量的构造方法
public ArrayList(int initialCapacity) {}
// 使用默认容量的构造方法
public ArrayList() {}
//把 Collection 参数 当作参数初始化的构造方法,如果参数为空,则使用默认的容量初始化
public ArrayList(Collection<? extends E> c) {}
复制代码
看完了构造方法,我们接下来看下主要的字段
/**
* 默认初始容量
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* 用于空实例的共享空数组实例
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
/**
* 用于默认大小的空实例的共享空数组实例。与EMPTY_ELEMENTDATA区分开来,以便在添加第一个元素时知道要扩容多少
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
* 主要存储结构。 ArrayList的容量是该数组的长度。
* 添加第一个元素时,任何带有elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA 的空 ArrayList都将扩展容量为 DEFAULT_CAPACITY,即默认容量 10
*/
transient Object[] elementData; // non-private to simplify nested class access
/**
* 这个 size 就是 上面数组的长度
*/
private int size;
复制代码
而我们对 ArrayList 的各种操作,其实就是在操作 elementData 数组。
接下来我们看我们最常用的 add 方法和 remove 以及 get 方法。
add 方法
/**
* 元素追加到此列表的末尾
*/
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
/**
* 将指定元素插入此*列表中的指定位置
*/
public void add(int index, E element) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // Increments modCount!!
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
复制代码
上述方法是添加一个元素的操作
/**
* 将指定集合中的所有元素追加到列表的末尾,按顺序返回一个指定的 collection 迭代器
* (如果在操作正在进行时修改了指定的集合,则此操作的行为是 undefined)
*/
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;
}
/**
* 从指定位置开始,将指定集合中的所有元素插入到列表的末尾,如果当前位置有元素,则后移,增加索引
*/
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;
}
复制代码
上述方法是添加一个集合的操作
1、 add()
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
// 数组赋值
elementData[size++] = e;
return true;
}
复制代码
我们来看第一个 add 方法,在该方法中,它首先调用 ensureCapacityInternal(size + 1) 来确保数组的容量是足够的,然后给指定索引下赋值,返回 true。我们来看下 ensureCapacityInternal 代码:
/**
* 维护 Object[] elementData 的容量大小
*/
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
/**
* 计算容量
*/
private static int calculateCapacity(Object[] elementData, int minCapacity) {
// 如果当前的 Object[] elementData 是 空的
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
//那么容量就是 在传入的容量和默认初始容量中选择一个最大值
// 所以 ArrayList 初始化后,其实只是把容量的大小确定了,而并没有去真正的实例化一个数组
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
/**
* 通过拿到具体的容量大小后,这里才真正的保证容量的大小
*/
private void ensureExplicitCapacity(int minCapacity) {
// 这个是对当前数组的变更次数的更新,每次添加,删除都会对该变量进行更新,我们在下边会将到该字段的具体作用
modCount++;
// 如果容量小于当前数组的长度,那么进行扩容
if (minCapacity - elementData.length > 0)
// 具体的扩容规则
grow(minCapacity);
}
/**
* 具体的扩容规则
*/
private void grow(int minCapacity) {
// 获取到当前数组的容量,这里可以看出,调用的是数组 length 方法计算长度
int oldCapacity = elementData.length;
// oldCapacity >> 1 代表是当前容量 除以2,位运算是高效操作
// 所以扩容的长度,就是当前长度的 1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 如果计算出的新的长度还是小于需要扩容的长度,那么直接与扩容的长度为准
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 如果计算出的新的长度比 数组的最大长度都大了,那么进行最大长度的计算,(数组的最大长度为 Integer.MAX_VALUE - 8 , 即 0x7fffffff (2 的 31次 -1 ) - 8)
if (newCapacity - MAX_ARRAY_SIZE > 0)
// 最大容量的判断
newCapacity = hugeCapacity(minCapacity);
// 数组拷贝
// 这里使用原数组和 newCapacity 这个最小容量 进行 copy, 因为最小容量更接近数组
// 使用的 Arrays 类进行的 copy,其最后调用的 仍然是 System.arraycopy 。
elementData = Arrays.copyOf(elementData, newCapacity);
}
/**
* 最大长度计算,只是一个最大容量的判断
*/
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
// 最小容量大于数组最大容量,则用 Inter 的最大长度,反之,用最大容量
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
复制代码
看完上面的 add 方法的具体实现,想必大家都很清晰了,我们再来总结下,添加一个元素 -> 确保容量 -> 数组 copy。很简单!
2、 add(int index, E element)
public void add(int index, E element) {
// index 的验证
rangeCheckForAdd(index);
// 确保容量
ensureCapacityInternal(size + 1); // Increments modCount!!
//系统的底层 拷贝
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
复制代码
这个 add 方法和上面的唯一区别就是 index 的验证 和 系统底层 copy 的方法使用不同,其他一样。
3、 add(Collection<? extends E> c)
/**
* 在列表的末尾追加指定的集合
*/
public boolean addAll(Collection<? extends E> c) {
Object[] a = c.toArray();
int numNew = a.length;
//容量的确保
ensureCapacityInternal(size + numNew); // Increments modCount
// 底层 copy
System.arraycopy(a, 0, elementData, size, numNew);
size += numNew;
return numNew != 0;
}
复制代码
4、 add(int index, Collection<? extends E> c)
/**
* 在指定位置追加集合
*/
public boolean addAll(int index, Collection<? extends E> c) {
//check 追加的索引
rangeCheckForAdd(index);
Object[] a = c.toArray();
int numNew = a.length;
//q确保容量
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;
}
复制代码
get 方法
/**
* 返回一个指定下标的元素
*/
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
/**
* 检查下标
*/
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
复制代码
get 方法很简单,主要是检查下标,然后直接返回。
remove 方法
在 Java 8 中,remove 方法主要有以下4种,供我们使用:
public E remove(int index) {}
public boolean remove(Object o) {}
public boolean removeAll(Collection<?> c) {}
public boolean removeIf(Predicate<? super E> filter) {}
复制代码
我们来看具体源码
/**
* 移除指定下标的元素
* 移除后,指定位置的元素向左移一位
*/
public E remove(int index) {
//检查给定的索引是否在范围内
rangeCheck(index);
//变更次数 自增
modCount++;
//获取元素
E oldValue = elementData(index);
//计算需要变更的元素
int numMoved = size - index - 1;
if (numMoved > 0)
//本地 拷贝
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
// index 下的元素 删除,即变更为null
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
复制代码
普通移除下标的方法很简单,主要是检查下标是否在范围内,然后调用系统底层函数进行数组的 copy,然后把传入的下标值设置为 null,等待 GC 即可。
所以 我们在移除一个下标元素的时候,如果下标越靠前,那么花费的时间越大,即最大时间为 O(n),而下标越靠后,则花费时间越少,最小时间为O(1)。
/**
* 移除第一个匹配到的元素,ArrayList 是可以插入 Null 的
*
*/
public boolean remove(Object o) {
//优先判断 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;
}
复制代码
移除指定元素的时候,由于 ArrayList 是可以插入Null 值的,所以需要先判断 Null 的情况;在移除指定元素的时候,调用了 fastRemove() ,如下:
/*
* Private remove method that skips bounds checking and does not
* return the value removed.
*/
private void fastRemove(int index) {
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
}
复制代码
就是一个简单的调用底层数组拷贝,移动下标,然后把相关下标下的元素设为 null。
/**
* 删除集合中包含的所有元素
*/
public boolean removeAll(Collection<?> c) {
Objects.requireNonNull(c);
return batchRemove(c, false);
}
/**
* 批量删除
*/
private boolean batchRemove(Collection<?> c, boolean complement) {
final Object[] elementData = this.elementData;
//r 是记录整个数组下标的, w是记录有效元素索引的
int r = 0, w = 0;
boolean modified = false;
try {
//便利数组
for (; r < size; r++)
//如果complement为false 相当于是取c在elementData中的补集,集合 c 包含则不记录,集合 c不包含则记录
//complement为true 相当于是取 c 和 elementData 的交集,c包含则记录,c不包含则不记录
if (c.contains(elementData[r]) == complement)
//r是正在遍历的位置 w是用于记录有效元素的 在w之前的全是有效元素,w之后的会被"删除"
elementData[w++] = elementData[r];
} finally {
// Preserve behavioral compatibility with AbstractCollection,
// even if c.contains() throws.
// 如果上面在遍历的过程中出错了,那么r肯定不等于size,于是源码就将出错位置r后面的元素全部放到w后面
if (r != size) {
System.arraycopy(elementData, r,
elementData, w,
size - r);
w += size - r;
}
// 如果w是不等于size,那么说明是需要删除元素的 否则不需要删除任何元素
if (w != size) {
// clear to let GC do its work
// 将w之后的元素全部置空 因为这些已经没用了,置空方便GC回收
for (int i = w; i < size; i++)
elementData[i] = null;
// 记录当前有效元素
modCount += size - w;
size = w;
modified = true;
}
}
return modified;
}
复制代码
在删除一个Collection集合的时候,从上面代码就可以看出,很长。其中我加了相关注释,方便大家阅读。其实它的主要操作就下面几点:
- 集合 c 的判断
- 调用内部的 batchRemove() 去删除本列表与 集合 c 的交集
- 循环便利,记录 c 中没有的元素,放到数组的前面,这个时候的临界点下标就是w,w 前面是留下的数组,后面是要删除的数组
- 在 第3 步出错的情况下,不删除元素,即跳到 finally 语句块
- 最后一步就是把 w之后的元素 设置为null,然后把 size 大小设置为 w,修改modCount
其实这个操作看起来麻烦,说白了就是
- 判断参数
- 便利交集
- 把无交集的下标元素,放到前面,然后这个下标位置设置为 w,之后的是有交集的,需要删除
- 删除元素,修改相关size 和modCount
/**
* 按照一定规则过滤集合中的元素
* @Param filter ,过滤规则
*/
@Override
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;
// 创建一个 与本List 大小相同的位集
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];
//lambda 表达式进行判断
if (filter.test(element)) {
//增加下标到位集中
removeSet.set(i);
removeCount++;
}
}
//这里判断如果修改次数不是预期次数,直接报并发修改异常
//也就是防止在外界使用List的时候,如果for 循环或者是 while 循环删除元素,直接报错
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
// shift surviving elements left over the spaces left by removed elements
//这里判断,如果传入的 Lambad 表达式过滤出结果,继续执行
final boolean anyToRemove = removeCount > 0;
if (anyToRemove) {
final int newSize = size - removeCount;
//这里的一个小算法就是把过滤出的元素,替换到后面
for (int i=0, j=0; (i < size) && (j < newSize); i++, j++) {
i = removeSet.nextClearBit(i);
elementData[j] = elementData[i];
}
//找到的元素,清空
for (int k=newSize; k < size; k++) {
elementData[k] = null; // Let gc do its work
}
this.size = newSize;
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
modCount++;
}
return anyToRemove;
}
复制代码
至于使用 Lambda 方法删除元素,我们在看了上面的 removeAll 方法后,就比较简单了:
- 创建位集
- 记录匹配的下标到位集中
- 判断是否被外界函数修改
- 判断是否有过滤结果
- 把匹配到的元素放到数组后面
- 进行移除
- 判断是否被外界函数修改
我们在经历了上面的摧残后,再看下面的代码就很easy了:
clear 方法
public void clear() {
modCount++;
// clear to let GC do its work
for (int i = 0; i < size; i++)
elementData[i] = null;
size = 0;
}
复制代码
clear 方法就是循环设置为null。
set 方法
public E set(int index, E element) {
//判断下标
rangeCheck(index);
//获取旧值
E oldValue = elementData(index);
//设置新值
elementData[index] = element;
return oldValue;
}
复制代码
ArrayList 中的其他方法
ArrayList 中还有一些其他方法,包括返回数组,返回只读集合,我们一起来看下:
返回数组 toArray
public Object[] toArray() {
return Arrays.copyOf(elementData, size);
}
public <T> T[] toArray(T[] a) {
if (a.length < size)
// Make a new array of a's runtime type, but my contents:
return (T[]) Arrays.copyOf(elementData, size, a.getClass());
System.arraycopy(elementData, 0, a, 0, size);
if (a.length > size)
a[size] = null;
return a;
}
复制代码
- 第一个方法返回的是 Object 类型的数组
- 第二个方法返回额是 对应类型的数组,如果参数数组长度够大,就用该数组,否则新建一个数组返回
优化数组大小的 trimToSize
public void trimToSize() {
modCount++;
if (size < elementData.length) {
elementData = (size == 0)
? EMPTY_ELEMENTDATA
: Arrays.copyOf(elementData, size);
}
}
复制代码
这个方法会重新分配一个数组,以节省空间。
通过iterator()遍历 修改操作
首先我们看下概念:fail-fast
fail-fast 机制是java集合(Collection)中的一种错误机制,当某一个线程A通过iterator去遍历某集合的过程中,若该集合的内容被其他线程所改变了;那么线程A访问集合时,就会抛出ConcurrentModificationException异常,产生fail-fast事件。
这个fail-fast 体现在我们上面经常提到的一个变量 ==modCount== 。
==ConcurrentModificationException== :
当不允许进行此类修改时,检测到对象的并发修改的方法可能抛出此异常
/**
* ArrayList 内部的 Iterator
*/
private class Itr implements Iterator<E> {
//下一个元素的指引
int cursor;
//当前访问的最后一个元素的索引
int lastRet = -1;
int expectedModCount = modCount;
Itr() {}
//判断是否有下一个元素
public boolean hasNext() {
return cursor != size;
}
//获取下一个元素
public E next() {
//判断本列表是否被其他线程修改
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() {
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();
}
}
//遍历列表
@Override
@SuppressWarnings("unchecked")
public void forEachRemaining(Consumer<? super E> consumer) {
Objects.requireNonNull(consumer);
final int size = ArrayList.this.size;
int i = cursor;
if (i >= size) {
return;
}
final Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length) {
throw new ConcurrentModificationException();
}
while (i != size && modCount == expectedModCount) {
consumer.accept((E) elementData[i++]);
}
// update once at end of iteration to reduce heap write traffic
cursor = i;
lastRet = i - 1;
checkForComodification();
}
//在遍历过程中判断是否被其他线程修改
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
复制代码
小结
ArrayList 内部是动态数组实现,所以:
- 随机访问,效率很高, 为O(1)
- 在非排序的情况下,效率很低,为 O(N)
- 添加元素效率一般,虽然在数组末尾添加的效率为O(1),头部添加的效率为O(N),但是重新分配和复制数组的开销被平摊了
- 插入和删除 为 O(N),毕竟要移动数组