ArrayList源码阅读
成员变量和静态常量
不考虑从AbstractList中继承成的modCount
之类的字段,ArrayList具有以下成员变量:
字段名 | 类型 | 说明 |
---|---|---|
elementData | Object[] | 存储元素的数组缓冲区 |
size | int | 当前元素数量 |
包含静态常量的完整定义如下:
1 |
|
serialVersionUID,DEFAULT_CAPACITY,elementData,size
这四个属性整体中规中矩,一看就知道什么意思。
具有迷惑性的还是如何区分EMPTY_ELEMENTDATA```和```DEFAULTCAPACITY_EMPTY_ELEMENTDATA
。
简单来说,就是 DEFAULTCAPACITY_EMPTY_ELEMENTDATA
和 EMPTY_ELEMENTDATA
虽然都是空数组,但用途不同。
前者用于默认构造的 ArrayList,更多的是标识此List是通过无参构造器构造的;后者用于容量为0的特殊情况。
构造器
ArrayList有三个构造器
传入一个初始容量的构造器
1 | /** |
代码整体比较简单,就三种情况。传入一个初始容量:
- 如果大于0就创建一个指定大小的数组
- 如果等于0就使用共享的表示空List的变量
EMPTY_ELEMENTDATA
- 如果小于0就抛出异常,因为数组的索引不能为零。
此时,ArrayList的初始size已经变为了对应传入大小。注意与下文无参构造器的对比。
无参构造器
1 | /** |
无参构造器就是简单的讲数组元素赋值为默认的表示无参构造器构造的DEFAULTCAPACITY_EMPTY_ELEMENTDATA
。
注意,尽管源码上的注释都写着初始化一个初始容量为10的空数组,但其实此时ArrayList的size为0,
目的是为了保持延迟初始化。只有在添加第一个元素时才会扩容到默认的容量10。
Collection的构造器
1 | /** |
通过传入的 Collection 创建一个新的 ArrayList,并按集合的迭代器顺序保存元素。
具体步骤很简单:
- 首先将集合 c 转为数组 a。
- 如果集合不为空,会判断集合的类型,如果本身就是 ArrayList 类型,直接修改引用指向复用其内部数组(提升效率)。
否则,复制一份新数组,防止外部修改影响内部数据。
如果集合为空,则使用共享的空数组 EMPTY_ELEMENTDATA。
此时,ArrayList若是不为空,则size已经调整为传入集合的大小。
具体用于复制数组的Arrays类的copyOf源码也很简单:
- 如果 newLength 大于原数组长度,则新数组多余部分用 null 填充。
- 如果 newLength 小于原数组长度,则只复制前 newLength 个元素。
- 新数组的类型由 newType 决定,支持任意数组类型。
- 对于 Object[] 类型,直接 new;否则用反射创建。
- 用 System.arraycopy 进行高效复制。
1 | /** |
公共方法
trimToSize
该方法主要是将内部数组容量缩减为当前元素数量以释放多余内存。
1 | /** |
首先递增来自于父类的 modCount 来标记结构性修改(主要是用于 fail-fast 检测以便尽可能快速地抛出ConcurrentModificationException)。
仅在当前容量大于实际大小时进行操作:
- 若 size == 0,把 elementData 指向共享的 EMPTY_ELEMENTDATA 空数组
- 否则用 Arrays.copyOf 按当前大小复制数组。
该方法在需要复制时,时间复杂度为 O(n)
该方法与 ArrayList 的其它方法一致,线程不安全,在并发环境需外部同步,否则可能抛出 ConcurrentModificationException 或产生不确定行为。
ensureCapacity
这个方法主要的作用就是在我们明确知道要添加多少元素时,可以提前扩容以减少多次扩容带来的性能损耗。
1 | /** |
首先递增来自于父类的 modCount 来标记结构性修改。
仅在当前容量大于实际大小时进行操作:
只有当请求容量大于当前数组长度时才会扩容;并且当当前数组是用于默认构造器的DEFAULTCAPACITY_EMPTY_ELEMENTDATA
且请求容量不超过默认容量 DEFAULT_CAPACITY 时,(也就是通过无参构造器构造的时候)保持延迟初始化(不提前分配),以便首次添加元素时按默认策略扩容
grow
grow是ArrayList的私有方法,主要用于实际执行扩容逻辑。
1 | /** |
这个方法是ArrayList中相当重要的的一个方法,主要是实现了扩容的功能。因此,我们要重点分析一下它的实现。
首先,它是记录了一下当前容量的大小,然后再进行下面的操作。
如果当前容量大于0,或者当前数组不是DEFAULTCAPACITY_EMPTY_ELEMENTDATA,前面说明过,DEFAULTCAPACITY_EMPTY_ELEMENTDATA是一个被final修饰的空数组,在三个构造方法中,只有无参构造方法中elementData被赋予了DEFAULTCAPACITY_EMPTY_ELEMENTDATA。
首先先看else分支,它只会处理的唯一的一种情况——用默认无参构造方法创建的数组的初始扩容情况
此时的容量为0,添加一个元素时会创建一个新的数组,其大小为max(DEFAULT_CAPACITY, minCapacity)。
从上面的源码变量信息中可得知DEFAULT_CAPACITY是一个常量,其值为10,而minCapacity的值为(0+1) 。
所以添加一个元素时,max(DEFAULT_CAPACITY, minCapacity)的值必为10。也就是说,当我们用默认无参构造方法创建的数组在添加元素前,ArrayList的容量为0,添加一个元素后,ArrayList的容量就变为10了。
看完了else,我们在看看if,来看看能处理其余的扩容情况的if语句是怎么做的。
我们来看一下if里面的操作,先创建一个新的数组,然后将旧数组拷贝到新数组并赋给elementData返回。ArraysSupport.newLength函数的作用是创建一个大小为oldCapacity + max(minimum growth, preferred growth)
的数组。
minCapacity是传入的参数,我们上面看过,它的值是当前容量(老容量)+1,那么minCapacity - oldCapacity的值就恒为1,minimum growth的值也就恒为1。
oldCapacity >> 1的功能是将oldCapacity 进行位运算,右移一位,也就是减半,preferred growth的值即为oldCapacity大小的一半。
也就是说,在用户没有干预,没有添加大量元素的情况下,自动扩容的规则是:第一次容量增长为10,后续每一次扩容都是当前容量的1.5倍(只是近似,因为右移碰上奇数会舍弃小数部分)
grow扩容的一点题外话
当oldCapacity为0时,右移后还是0,也就是说此时扩容的大小为0+max(1,0)=1,容量从0扩展到1。那么什么时候是这种情况呢?
用户调用了传容量的构造方法,并且传入的是0,此时elementData被赋予的是EMPTY_ELEMENTDATA,此时数组容量为0,添加元素时,符合if的条件,会进入此扩容情况,容量从0扩展到1。
传Collection元素列表的构造方法被传入空列表时,elementData被赋予的是EMPTY_ELEMENTDATA,数组容量为0,此时添加元素时,符合if的条件,会进入此扩容情况,容量从0扩展到1。