【Java集合框架】3 - 4 ArrayList 源码分析
§3-4 ArrayList
源码分析
3-4.1 ArrayList
集合
ArrayList
是 List
接口的一个实现类,其本质是一个可变长的数组。
基于这种实现关系,ArrayList
类对象可以调用 List
以及 Collection
接口的方法。
该类并不同步,因此早多线程中使用同一个 ArrayList
实例,且至少一个线程对该列表做了结构性修改,则必须外部同步。
结构性修改指的是添加或删除一个或多个元素、或显式重新调整数组大小的操作,仅仅修改元素的值并不是结构性修改。
由于该实现类中的方法大多重写自其所实现的接口以及父接口,此处不过多赘述。该类方法中最主要的操作就是增删改查。
3-4.2 ArrayList
源码解析
ArrayList
的底层原理:
-
使用无参构造创建的
ArrayList
实例,默认创建的是长度为零的数组;ArrayList
实例中的数组由成员elementData
存储,该无参构造器的实现为:public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; }
而
DEFAULTCAPACITY_EMPTY_ELEMENTDATA
是内部定义的一个静态常数组,其长度为零,用于给默认大小空实例中的数组赋值。ArrayList
中有另外定义了一个长度为零的静态常空数组EMPTY_ELEMENTDATA
,该数组用于空实例,不同于前者。 -
此时,添加第一个数据,底层会创建一个新的长度为 10 的数组;
类中还定义了一个整型变量
size
,用于记录当前数组中元素个数,并指定下一次存放元素的位置;add(E e)
方法实现:public boolean add(E e) { modCount++; add(e, elementData, size); return true; }
私有变量
modCount
来自于抽象类AbstractList
,用于记录列表被结构性修改的次数。该字段由iterator
和listIterator
方法返回的迭代器和列表迭代器使用。若该字段的值意外改变,(列表)迭代器则会在调用next
,remove
,previous
,set
或add
方法时抛出异常ConcurrentModificationException
。方法底层调用了另外一个私有的多参数重载
add
方法,该方法的实现为:private void add(E e, Object[] elementData, int s) { if (s == elementData.length) elementData = grow(); elementData[s] = e; size = s + 1; }
使用无参构造所创建的集合对象中,
size
为0
,elementData
是一个长度为零的数组,满足选择分支中的条件语句,执行grow
方法扩容数组,该方法的实现为:private Object[] grow() { return grow(size + 1); }
内部又调用了另外一个有参重载,其实现为:
private Object[] grow(int minCapacity) { int oldCapacity = elementData.length; if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { int newCapacity = ArraysSupport.newLength(oldCapacity, minCapacity - oldCapacity, /* minimum growth */ oldCapacity >> 1 /* preferred growth */); return elementData = Arrays.copyOf(elementData, newCapacity); } else { return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)]; } }
形参表示扩容后数组的理论最小容量,首次添加元素,则
minCapacity == 1
;方法会先记录原数组的旧容量,然后判断,由于调用的是无参构造,旧容量为
0
,直接来到else
分支,返回一个新的数组;新数组的长度取
DEFAULT_CAPACITY
(默认容量)和最小容量的最小值,而前者定义在类中,为10
;因此,对于无参构造获得的集合,首次添加元素(一个)会将数组扩容至长度
10
,grow
方法结束,回到add
方法,将该元素添加进数组,并让size
更新(为1
)。至此,首次添加元素结束; -
数组存满时,数组容量会扩容 1.5 倍;
数组存满时,仍然会通过上述流程执行有参
grow
方法;以旧容量为
10
为例,先记录旧容量,由于旧容量此时不为0
,短路或运算结果为true
,进入if
选择分支;此时,通过
newLength
方法计算数组的新容量,该方法实现为:public static int newLength(int oldLength, int minGrowth, int prefGrowth) { // preconditions not checked because of inlining // assert oldLength >= 0 // assert minGrowth > 0 int prefLength = oldLength + Math.max(minGrowth, prefGrowth); // might overflow if (0 < prefLength && prefLength <= SOFT_MAX_ARRAY_LENGTH) { return prefLength; } else { // put code cold in a separate method return hugeLength(oldLength, minGrowth); } }
其中,
SOFT_MAX_ARRAY_LENGTH = Integer.MAX_VALUE - 8 = 2_147_483_639
;prefLength
,即首选长度通过计算变为15 = 10 + max(1,10 >> 1) = 10 + max(1,5) = 10 + 5
,且满足第一个分支语句,直接返回,作为新数组的新容量;接着,再通过
Arrays.copyOf
方法,拷贝原始数组到一个具有新容量的数组中,并将该新数组返回;grow
方法结束,在新数组中添加该元素,并更新size
(为11
),添加完成; -
若一次添加多个元素,1.5 倍扩容仍然不足,则新数组长度以实际为准;
添加多个元素,往往用的可能是
addAll
方法,其实现为:public boolean addAll(Collection<? extends E> c) { Object[] a = c.toArray(); modCount++; int numNew = a.length; if (numNew == 0) return false; Object[] elementData; final int s; if (numNew > (elementData = this.elementData).length - (s = size)) elementData = grow(s + numNew); System.arraycopy(a, 0, elementData, s, numNew); size = s + numNew; return true; }
方法首先会将入参的集合使用接口中的
toArray
方法,将集合中的内容转换为待添加数组,并记录该待添加数组的长度;这里设待添加数组长度为
100
,远远大于 1.5 倍的扩容大小,满足if
分支中的表达式要求,调用有参grow
扩容;在
grow
中的if
分支中,调用相同方法计算新容量(newLength(15, (11 + 100) - 15, 7)
),返回的newCapacity = prefLength = 15 + max(96, 7) = 111
;然后,拷贝数组,将新数组返回后,添加新元素,更新
size
(为111
),返回true
,表示成功添加,方法结束;