参考资料:
《Java集合:ArrayList详解》
《Collection - ArrayList 源码解析》
《ArrayList》
写在开头:本文为个人学习笔记,内容比较随意,夹杂个人理解,如有错误,欢迎指正。
ArrayList是我们工作中接触的最多的集合之一(另一个是HashMap),研究下源码对我们的
帮助还是很大的。
目录
一、基础概念
二、构造方法
三、查找和修改
1、查找
2、修改
四、删除
1、remove(int index)
2、remove(Object o)
3、clear
注意点
(1)modCount++;
(2)elementData[--size] = null;
五、增加
六、扩容
1、前置判断
2、执行扩容
我们知道,数组Array需要在申明时定义大小,这样才能在运行前在内存中为其开辟空间。但是我们有时候会遇到需要拓展的情况,这个时候我们只能重新开辟一个更大容量的数组来接力,工具类Arrays中提供了copyOf方法来实现。
int[] array1 = new int[10];
int[] array2 = Arrays.copyOf(array1, 20);
虽然看起来解决了问题,但是系统在运行时是没办法预知数组的容量的,比如将数据库中查询的结果装在入一个数组中这种场景。为了解决此类问题,List便应运而生。
我们先来看一眼集合框架的整体结构
我们看到List主要有2个实现类,分别是ArrayList和LinkedList,这两者的区别主要在于前者是基于数组的,后者是基于链表的。其中ArrayList通过不断的扩容实现了动态数组的功能,当容量不足时便会去扩容。
首先是成员变量,我们可以看到其本质还是Object数组。
// Object对象数组,因为只有Object引用才能指向所有对象,transient 表示不序列化
transient Object[] elementData;
// 初始容量,默认为10
private static final int DEFAULT_CAPACITY = 10;
// 空数组
private static final Object[] EMPTY_ELEMENTDATA = {};
// 和上面一样的空数组,不过会在第一次调用ensureCapacityInternal时会初始化为默认容量
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 数组当前的元素数量,注意和数组长度的区别
private int size;
// 数组的最大长度,这里-8是因为有些JVM里会在数组头部放一些属性信息
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
// 父类AbstractList中的成员变量,用来记录结构的变化(注意,数组内元素变化不属于此类)
protected transient int modCount = 0;
只有在明确了初始化数组大小的时候才会创建数组,否则都会以[]来代替,将真正的创建数组延迟到扩容中执行。
public ArrayList() {
// 不指定初始化大小,则标记为DEFAULTCAPACITY_EMPTY_ELEMENTDATA
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
// 指定了初始化大小,则直接创建Object数组
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
// 初始化容量为0时,标记为EMPTY_ELEMENTDATA
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
public ArrayList(Collection extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}
第一节我们了解到,ArrayList的本质是Object数组,那么也就是说,修改和查找可以基于数组的特性来完成。
public E get(int index) {
rangeCheck(index); // 检查index是否数组越界
return elementData(index); // 调用下方elementData方法
}
@SuppressWarnings("unchecked")
E elementData(int index) {
// 直接获取数组内index下标位置元素(注意这里的类型转换,)
return (E) elementData[index];
}
public E set(int index, E element) {
// 校验下标是否越界
rangeCheck(index);
// 下面这段,先通过elementData方法获取当前元素,然后再覆盖
E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}
以上就是ArrayList的查找和修改,非常的简单,不过我们依然可以看出严谨的思路,比如开始的下标越界校验,再比如set方法的将旧元素取出再覆盖。
相比于查找和修改,删除就要复杂一些,也拥有更多的变形。
// 删除index位置的元素,并将后续所有位置上元素向左移动1位
public E remove(int index) {
rangeCheck(index);
// 结构变动次数+1
modCount++;
// 和set方法一样,先取出要删除的元素
E oldValue = elementData(index);
// 计算出需要移动的元素个数(注意,这里-1是因为数组从0开始计数,最后一个元素下标=元素个数-1,所以这里是为了消除这个多出来的1)
int numMoved = size - index - 1;
// 大于0表明有元素需要移动
if (numMoved > 0)
// 被删除元素右侧位置全部左移一位
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
// 将最后一位置空(这里有一个细节,下面会单独拎出来解释下)
elementData[--size] = null;
// 返回被删除元素
return oldValue;
}
// 与之前的remove不同,这个删除指定元素,不过只能删除第一个匹配到的,匹配不到则返回false
public boolean remove(Object o) {
// 需要先做非空判断,因为null无法使用.equals方法
if (o == null) {
for (int index = 0; index < size; index++)
// 遇到第一个匹配到的对象便进行删除
if (elementData[index] == null) {
// 调用fastRemove快速删除
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++; // 修改次数+1
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null;
}
// 清空数组内元素
public void clear() {
// 记录结构变化次数+1
modCount++;
// 依次将数组内元素设置为null元素
// clear to let GC do its work
for (int i = 0; i < size; i++)
elementData[i] = null;
// 数组大小不变,但将容量归0
size = 0;
}
以上就是三个删除相关的方法,并不难理解,不过还是有几点需要关注的。
开头我们介绍modCount的时候解释过它是用来记录结构变化次数的,这是因为ArrayList不是线程安全的,即如果多个线程同时操作一个ArrayList是可能导致异常的。ArrayList采用了快速失败的机制(Fail-Fast),通过记录modCount参数来实现。在面对并发的修改时,迭代器很快就会完全失败,而不是冒着在将来某个不确定时间发生任意不确定行为的风险。
Fail-Fast是Java集合的一种错误检测机制。当遍历集合的同时修改集合或者多个线程对集合进行结构上的改变的操作时,有可能会产生fail-fast机制,记住是有可能,而不是一定。其实就是抛出ConcurrentModificationException 异常。
集合的迭代器在调用next()、remove()方法时都会调用checkForComodification()方法,该方法主要就是检测modCount == expectedModCount ? 若不等则抛出ConcurrentModificationException 异常,从而产生fail-fast机制。modCount是在每次改变集合数量时会改变的值。
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
这一行的功能不难理解,将最后一个元素置为null。不过这里有一个妙处,我们看clear方法中的清除过程,如下图所示
将清除工作交给GC来完成,实际上,我们使用size--也能实现效果,但是这样做的后果是内存中引用和对象的依赖关系依然存在,GC并不会去主动回收。将元素赋值为null的好处在于将该位置的引用清除,这个时候GC在定期清理引用为0的对象时就会请这块内存中的对象清除掉。
添加元素有4个方法,add(E e)、add(int index, E element)、addAll(Collection extends E> c)、addAll(int index, Collection extends E> c)。前两个是加入单个元素,后两个是将集合加入进去。相同的点在于,所有插入方法都会在开始add前,调用ensureCapacityInternal方法进行扩容判断。
public boolean add(E e) {
// 扩容判断
ensureCapacityInternal(size + 1);
// 将新增元素放置在扩容后的第一个位置上
elementData[size++] = e;
return true;
}
public void add(int index, E element) {
rangeCheckForAdd(index);
// 扩容判断
ensureCapacityInternal(size + 1);
// 将该位置及其右侧位置元素全部右移一位
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
public boolean addAll(Collection extends E> c) {
Object[] a = c.toArray();
int numNew = a.length;
// 扩容判断
ensureCapacityInternal(size + numNew);
// 将旧数组a放入到新数组中,从size位置开始放置
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;
}
// 公开方法,可外部调用,即允许我们手动扩容
public void ensureCapacity(int minCapacity) {
// minExpand为最小需要扩展的容量判断是否是DEFAULTCAPACITY_EMPTY_ELEMENTDATA
// 即无参构造方法的结果,如果是,则定义DEFAULT_CAPACITY,即默认10的大小,否则为0
int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
? 0 : DEFAULT_CAPACITY;
// minCapacity为所需要的最小容量,将其与minExpand比较,判断是否需要扩容
if (minCapacity > minExpand) {
ensureExplicitCapacity(minCapacity);
}
}
// 内部方法,即add中调用的扩容判断
private void ensureCapacityInternal(int minCapacity) {
// 如果当前为空,取默认大小与最小需要容量之前的较大值
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
// 调用后续方法
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// 最后判断数组大小是否满足最小容量,不足则需要扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
这一块逻辑稍复杂些,不过我们只需要记住这一整块的逻辑就是为了判断,当前数组的容量是否已无法再添加元素,此时,我们才真正进行扩容。
private void grow(int minCapacity) {
// 获取当前数组大小,并计算新数组大小,扩容规则为1.5倍
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 默认扩容后大小与所需数组大小比较,取较大值
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 如果新数组大小大于数组允许的最大容量,则需要进一步处理
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 调用Arrays.copyOf复制新数组
elementData = Arrays.copyOf(elementData, newCapacity);
}
private static int hugeCapacity(int minCapacity) {
// 溢出
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
// 判断minCapacity大于MAX_ARRAY_SIZE,是则返回Integer.MAX_VALUE,否则返回MAX_ARRAY_SIZE
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}