JAVA集合之ArrayList源码分析
1. 概述
ArrayList是一个动态数组序列,能动态增长和缩减容量。允许存入空值和重复元素,基于数组实现,具备快速随机查找功能。非线程安全。本文针对jdk1.8从源码角度分析ArrayList结构,初始化,容量,以及扩容。
2. 源码分析
2.1 继承结构
打开源码我们可以看到ArrayList继承的类与实现的接口
分析:
- 继承AbstractList,而AbstractList实现List接口
为何ArrayList不直接实现List接口?
思考一下,接口与抽象类的区别:接口中全是抽象方法,而抽象类可以有抽象方法,也可以有具体实现方法。
与ArrayList一样继承AbstractList的还有LinkedList和Vector,它们应该都有共同的通用方法,让这些通用方法在AbstractList里做具体实现,它们在自己的具体类中直接拿到这些通用方法,自己再实现自己特有的方法。这样能够减少代码重复,让代码更加简洁。这是我们在日常写代码时也应该具备的思维方式。 - 实现List接口
AbstractList已经实现了List接口,为何ArrayList还要再实现一次?
这个做法让我很迷惑。如果有找到答案的朋友,希望可以给我留言。 - 实现RandomAccess接口
查看文档说明
RandomAccess是标记接口,用来表示支持快速随机访问,文档中也说明实现了该接口后,for循环遍历性能最优。 - 实现Cloneable接口
实现Cloneable接口,就可以使用Object.clone()进行克隆了。 - 实现Serializable接口
实现序列化接口,说明该类能够启用其序列化功能。序列化其实就是持久化,将class转变为字节流传输,然后保存,反序列化就是读取。
2.2 类属性
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
// 版本号
private static final long serialVersionUID = 8683452581122892189L;
// 缺省容量
private static final int DEFAULT_CAPACITY = 10;
// 空对象数组
private static final Object[] EMPTY_ELEMENTDATA = {};
// 缺省空对象数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 元素数组
transient Object[] elementData;
// 实际元素大小,默认为0
private int size;
// 最大数组容量
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
}
2.3 构造方法
ArrayList有三个构造方法,分别为:
ArrayList()
ArrayList(int)
ArrayList(Collection<? extends E>)
2.3.1 无参构造
/**
* Constructs an empty list with an initial capacity of ten. 初始化容量为10
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
在构造这个ArrayList对象的时候,将缺省空数组对象赋予elementData,但是在空参构造函数的备注中却说明初始化容量为10,这一点在下面的add()方法分析。
2.3.2 有参构造1
允许传入一个int类型作为ArrayList的容量,实则都是初始化一个数组对象,该参数作为数组的长度。这里分别判断给定长度的数值大于零,等于零,小于零的情况,当然,数组长度不应该有负值,所以小于零的情况做了异常处理,抛出非法数值异常。
2.3.3 有参构造2
这里的参数是允许传入一个泛型Collection<>。作用就是所有继承自该泛型的子类都可以转化为ArrayList<>
elementData = c.toArray();这里做了数组转换
进而判断该长度不等于零、等于零的情况,这里做判断的原因是不同的Collection的toArray()的实现不同,要对Collection进行改造,变成我们想要的Object[]。
长度等于零的时候统一改造成之前已经定义好的EMPTY_ELEMENTDATA
长度不等于零的时候就判断该Collection属不属于Object[]类型,不属于就使用Array.copyOf重新进行改造
2.3.4 小结
ArrayList构造方法都是在初始化容器,而这个容器本质就是一个数组,在这里被定义为elementData
2.4 增
ArrayList主要的特点是动态扩容,增加元素操作有四个方法,这里只分析add()方法
2.4.1 add(e)
先看看add()方法
注释中说明add方法往list末尾追加一个元素
ensureCapacityInternal(size + 1); 添加了一个元素,容量变化+1
再看看这个方法怎么做的
private void ensureCapacityInternal(int minCapacity) {
//这里判断elementData是不是一个空数组
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
//如果是,则将minCapacity变成10,也就是先前定义好的DEFAULT_CAPACITY(默认容量)
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code //当前数组长度比数组elementData容量要大时调用grow()
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
看到这里,就可以理解在本文2.3.1无参构造中ArrayList作者为何说明容器初始容量为10了,到这里,很显然只是初始化容器容量的值minCapacity,还未对elementData数组初始化容量。接着看源码,当minCapacity - elementData.length > 0(存在剩余容量的情况)调用grow()方法,这个方法就很有趣了,接着看!
定义一个oldCapacity(旧容量)
int newCapacity = oldCapacity + (oldCapacity >> 1); 这是实现ArrayList动态扩容的核心步骤,oldCapacity>>1位移运算,相当于0.5倍,加上自身则为1.5倍增长
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
此处又将新容量newCapacity与minCapacity比较,哪个大就作为最新容量
当超过最大值(最大值为Integer最大值-8)时,调用hugeCapacity()
这里扩容也很简单,以MAX_ARRAY_SIZE作为临界,当前数组长度比 MAX_ARRAY_SIZE大时,容量扩展到Integer的最大值,否则扩展为MAX_ARRAY_SIZE
为什么是-8?而不是直接用Integer最大值,这里涉及到JVM的知识,数组作为对象,需要一定的内存存储自身的对象头信息,而对象头的信息最大占用内存为8 Bytes
再回到grow()最后一步
//到这一步才真正完成elementData的扩容
elementData = Arrays.copyOf(elementData, newCapacity);
ArrayList的扩容机制大致流程如下:
2.4.2 add(int,e)
先看add(int,e)源码
很明显,调用的rangeCheckForAdd(index)方法,是对index越界的检查
接着调用System.arraycopy()方法,看看该方法的文档说明
看到这里,不难理解ArrayList为何插入元素效率会低,每插入一个元素都会拷贝一份新的数组,同时进行元素的位移。当然这种结构带来的优势在于根据索引获取元素的效率杠杠地。
2.4.3 小结
add(e)方法会在集合末尾添加指定的元素,允许存入空值和重复元素,容量会以原容量的1.5倍动态扩展。
add(int, e)方法在指定位置插入指定元素,指定位置到末尾的元素会进行位移
2.5 删
删除操作有五个方法如下:
2.5.1 remove(int)
rangeCheck(index): 对下标做检查,越界异常处理
E oldValue = elementData(index); 保存好要删除的元素,函数结束时返回
int numMoved = size - index - 1; 计算出要移动的元素个数
删除元素也要进行位移,与插入元素原理一样,也是调用System.arraycopy()
最后将集合最后位置设置为null,上面作者的注释也可以看到,设置为null让GC快点把它干掉。
2.5.2 remove(Object)
remove(Object)方法允许传入Object类型的参数,说明ArrayList支持放入null值 这里做了遍历查看elementData中是否有指定的这个元素,会调用fastRemove(index)方法:
可以看到fastRemove(index)的实现基本和remove(index)一样,区别在于它没有返回值。
2.5.3 removeAll(Collection<?> c)
这是一个批量删除的操作,与之相似的还有另一个方法retainAll(Collection<?> c)
它们都调用了batchRemove()方法,我们看看这个方法做了什么
private boolean batchRemove(Collection<?> c, boolean complement) {
//将原数组在这里重新定义一份,并声明为final,出了该方法,新定义的elementData就无效了
final Object[] elementData = this.elementData;
int r = 0, w = 0; //r:控制循环 w:纪录交集元素个数
boolean modified = false;
try {
// 下面会用图解展示变化过程
for (; r < size; r++)
// conntains()判断 c集合是否包含elementData[r]元素
if (c.contains(elementData[r]) == complement)
// 这里complement用的非常精巧
// 当complement为true的时候(也就是调用retainAll()时),w代表的是纪录交集元素个数
// 当complement为false的时候(调用removeAll()时),w则代表的是无法删除的元素个数
elementData[w++] = elementData[r];
} finally {
// Preserve behavioral compatibility with AbstractCollection,
// even if c.contains() throws.
// 当contains()出现异常时,r才会不等于size,这里是为了兼容AbstractCollection中contains()方法的实现
if (r != size) {
System.arraycopy(elementData, r,
elementData, w,
size - r);
w += size - r;
}
//从w作为索引位置遍历逐个设置为null,让GC干掉
if (w != size) {
// clear to let GC do its work
// 下面会用图解展示变化过程
for (int i = w; i < size; i++)
elementData[i] = null;
modCount += size - w;
size = w;
modified = true;
}
}
return modified;
}
下面用图解示例— e: 原集合[1,2,3,4,5] c: 指定集合[2,5,6]
看看以上代码两个for循环的变化过程:
removeAll(Collection<?> c) 调用 batchRemove(c, true) complement为true:
retainAll(Collection<?> c) 调用 batchRemove(c, false) complement为false:
从以上图解可以看到removeAll会删除掉两集合交集的所有元素,retainAll则会让原集合只保留两交集部分,它们都会返回true,只有当原集合没有任何改动的时候,才会返回false
2.5.4 小结
remove方法,在删除指定下标的元素时,从下标位置到末尾的元素都会往前移位,同时将最后一个元素设置为null,让GC清理
removeAll方法会删除掉原集合中所有包含指定集合的元素
retainAll方法会删掉原集合与指定集合的补集,只保留交集
2.6 改
对ArrayList改操作有set(int, E) 和 indexOf(o) 方法(indexOf方法是获取元素的索引,这里归类为改操作),还有一个lastIndexOf(o) 方法原理与indexOf(o) 相似,下面分别分析前两个方法的源码:
2.6.1 set(int, E)
先上源码:
这里很容易看懂
rangeCheck(index) 这一步上面有阐述过,index的越界检查;
E oldValue = elementData(index); 定义一个oldValue未修改之前的元素,方法结束时作为返回值
elementData[index] = element; 将给定的新值赋值到index位置
2.6.2 indexOf(Object o)
源码:
indexOf(Object o) 方法传入指定元素获取该元素第一次出现位置的索引(ArrayList可放重复元素,所以这里为第一次出现的索引);源码对三种情况做了处理,指定值为null、指定值不为null、指定值不存在于该集合中,前两种情况使用了for循环,指定值不为null时使用Object的equals方法判断是否同一个元素,元素不存在于集合时,返回-1;
2.6.3 小结
改操作都是从头开始查找给定的元素,这里要注意,前面讲过ArrayList可以存入null值,所以null值也要考虑在内;indexOf和lastIndexOf方法在给定值不存在于集合时,返回-1
2.7 查
2.7.1 get()
这里说一下下标越界检查,上面源码可以看出rangeCheck只判断index >= size,那小于零的情况怎么办?作者在注释中说明了,index为负数的时候交给ArrayIndexOutOfBoundsException处理,ArrayIndexOutOfBoundsException继承自IndexOutOfBoundsException,它们都属于运行时异常
最后在返回元素的时候做了向下造型的处理
3. 总结
ArrayList 底层是数组实现
ArrayList 可以存放null值且值可重复
ArrayList 可以自动扩容,扩容因子为1.5倍(jdk1.7和jdk1.8)
ArrayList 查询效率很快,增删效率慢
ArrayList 实现了RandomAccess快速访问接口,for循环遍历最快
ArrayList 线程不安全,所有操作都不是原子性的,比如插入数据时,需要判断elementData容量是否满足,再进行对应位置赋值,多线程情况下会导致数据不一致的问题。