这一篇开始集合的总结,经过前两篇文章(fail-fast 以及 Spliterator)的铺垫,我们能更快的进入集合类的学习。本篇是集合类的开篇,也是比较简单的一篇。这次我们主要来看看ArrayList类的实现。看看他是如何存储元素以及扩容的。

结构分析
RandomAccess
首先,我们来看看ArrayList继承了啥,实现了啥

这里我们不对AbstractList进行讲解,不过就是一些List的基本操作。我们重点看一下RandomAccess。
查看RandomAccess的源码:

我们发现他什么都没干,那么这个接口是干什么的呢? 其实,从名字我们也能猜出来他是干啥的RandomAccess 即随机访问,源码中是这么介绍他的

一个标记接口,List实现它表示自己支持快速的(通常是恒定时间)随机访问。这个接口的主要目的就是允许算法去提供一个更好的执行性能当决定使用随机或者顺序访问的时候。
上面的随机访问即使用索引 index去访问,顺序访问则是使用Iterator这样迭代的方法去访问。
所以当算法去决定是不是使用随机访问比顺序访问的好的时候就可以用 instanceof RandomAccess 去判断。
存储结构
上面我们说这个ArrayList随机访问的性能好,那么为什么随机访问性能好呢? 我们来看看ArrayList中的几个属性。

- elementData: 这个就是ArrayList存储元素的地方,ArrayList的容量就是这个数组的长度。 其实之所以ArrayList的随机访问性能好就是因为其底层存储数据的是一个数组。按下标访问性能高效是数组天然的优势。
- EMPTY_ELEMENTDATA: 用于空实例的共享空数组实例。 当我们创建初始容量为 0 或者根据一个空的Collection去创建一个ArrayList 即调用 new ArrayList(0);或者new ArrayList(Collection<? extends E> c);(注意这个c.toArray().length==0)的时候elementData会被赋予EMPTY_ELEMENTDATA。
- DEFAULTCAPACITY_EMPTY_ELEMENTDATA: 用于默认大小的空实例的共享空数组实例。 之所以将这个与EMPTY_ELEMENTDATA区分开来,是因为方便在添加第一个元素时知道要扩容多少。 当调用 new ArrayList();的时候会将此赋予elementData;
- DEFAULT_CAPACITY: 默认初始容量,当不指定初始容量时使用,默认是10,即不指定初始容量的状态下,ArrayList的容量是10,超出此值便会进行扩容操作。
- size: 没什么特别的,记录ArrayList存储元素的数量。但是其与ArrayList的容量并不等价。ArrayList中有一个方法,代码如下:

意思就是调用这个方法后,elementData的数组的长度就会等于size的大小,即把数组的大小调整为存储当前数量元素实际需要的大小。
经过上面的分析,我们得出了ArrayList的存储结构是一个数组的形式
基本方法实现
添加元素
对于添加来说不过就是add方法,set方法。我们实际看一下代码:



我们先不管ensureCapacityInternal这个方法,这个是扩容机制的开始方法,下面我们会再提。 其实对于插入来说就是对数组的操作,有意思的是,在add(int index,E element)中使用了System.arraycopy方法来对数组进行移位操作。这是一个浅复制,有兴趣额的同学自己查查看吧。 删除操作其实都一样,就不做赘述了。
扩容机制
ArrayList的扩容机制其实挺简单的,我们先从代码入手看看,他是怎么完成这个扩容的。上面提到ensureCapacityInternal方法时扩容的开始,其实在每一个往elementData里面添加元素的方法中都先调用了这个方法,参数是增加当前元素的数量加上需要添加元素的数量。即ArrayList在添加元素的时候会检查是否需要扩容。我们先看看这个方法:

我们可以看到,这个方法先判断了一下elementData是否等于DEFAULTCAPACITY_EMPTY_ELEMENTDATA,即我们上面提到的使用new ArrayList();生成的ArrayList对象中elementData赋予的值,也就是检查一下,这个ArrayList是不是默认容量现在是DEFAULT_CAPACITY。
这个minCapacity的含义就是添加某个多多个元素后,elementData最小的长度。
之后这个方法又调用了ensureExplicitCapacity方法,我们现在看一下这个方法的内容:

modCount作用于快速失败机制,如果不了解可以看一下这篇文章。
之后判断了一下这个minCapacity的大小是否大于elementData的长度,即elementData的大小是不是已经不满足增加某个或者多个元素的要求了。
如果不满足则调用grow方法。现在就到了扩容的核心了,我们先看一下grow方法的内容:

其实就是对应该扩容后elementData的大小进行判断,默认增长为原来大小的1.5倍。因为是使用位移的方法计算原来elementData长度的一半,所以有可能oldCapacity>>1的结果为负数,这时候直接扩容为需要的大小。后面的不解释了,很简单。
要注意一点,由于elementData是数组,而数组的大小是不能改变的,所以扩容是重新创建了一个数组。
总结
- ArrayList底层存储结构是一个数组
- ArrayList扩容默认增长为原来的1.5倍,如果还不满足需要则直接增长为需要的长度。
- 按数组下标访问元素-get(i)\set(i,e)的性能很高,这是数组的基本优势。
- 直接在数组末尾加入元素-add(e)的性能也很高,但如果按下标插入、删除元素-add(i,e)\remove(i)\remove(e),则要用System.arraycopy()来移动部分受影响的元素,性能就变差了,这是基本劣势。