JDK 源码解析 —— 集合(一)数组 ArrayList
JDK 源码解析 —— 集合(一)数组 ArrayList
1. 概述
ArrayList ,基于 []
数组实现的,支持自动扩容的动态数组。相比数组来说,因为其支持自动扩容的特性,成为我们日常开发中,最常用的集合类,没有之一。
在前些年,实习或初级工程师的面试,可能最爱问的就是 ArrayList 和 LinkedList 的区别与使用场景。不过貌似,现在问的已经不多了,因为现在信息非常发达,这种常规面试题已经无法区分能力了。当然即使如此,也不妨碍我们拿它开刀,毕竟是咱的“老朋友”。
2. 类图
ArrayList 实现的接口、继承的抽象类,如下图所示:类图
实现了 4 个接口,分别是:
java.util.List
接口,提供数组的添加、删除、修改、迭代遍历等操作。-
java.util.RandomAccess
接口,表示 ArrayList 支持快速的随机访问。关于 RandomAccess 标记接口,我们这里先不展开,胖友可以自行阅读 《RandomAccess 这个空架子有何用?》 文章。
-
java.io.Serializable
接口,表示 ArrayList 支持序列化的功能。关于 Serializable 标记接口,我们这里先不展开,胖友可以自行阅读 《Java 序列化与反序列化》 文章。
-
java.lang.Cloneable
接口,表示 ArrayList 支持克隆。关于 Cloneable 标记接口,我们这里先不展开,胖友可以自行阅读 《在 Java 中为什么实现了 Cloneable 接口,就能调用 Object 的 clone 方法?》 讨论,特别是 R 大的回答。
继承了 java.util.AbstractList
抽象类,而 AbstractList 提供了 List 接口的骨架实现,大幅度的减少了实现迭代遍历相关操作的代码。可能这样表述有点抽象,胖友点到 java.util.AbstractList
抽象类中看看,例如说 #iterator()
、#indexOf(Object o)
等方法。
😈 不过实际上,在下面中我们会看到,ArrayList 大量重写了 AbstractList 提供的方法实现。所以,AbstractList 对于 ArrayList 意义不大,更多的是 AbstractList 其它子类享受了这个福利。
3. 属性
ArrayList 的属性很少,仅仅 2 个。如下图所示:
elementData
属性:元素数组。其中,图中红色空格代表我们已经添加元素,白色空格代表我们并未使用。size
属性:数组大小。注意,size
代表的是 ArrayList 已使用elementData
的元素的数量,对于开发者看到的#size()
也是该大小。并且,当我们添加新的元素时,恰好其就是元素添加到elementData
的位置(下标)。当然,我们知道 ArrayList 真正的大小是elementData
的大小。
对应代码如下:
// ArrayList.java
|
4. 构造方法
ArrayList 一共有三个构造方法,我们分别来看看。
① #ArrayList(int initialCapacity)
#ArrayList(int initialCapacity)
构造方法,根据传入的初始化容量,创建 ArrayList 数组。如果我们在使用时,如果预先指到数组大小,一定要使用该构造方法,可以避免数组扩容提升性能,同时也是合理使用内存。代码如下:
// ArrayList.java
|
- 比较特殊的是,如果初始化容量为 0 时,使用
EMPTY_ELEMENTDATA
空数组。在添加元素的时候,会进行扩容创建需要的数组。
② #ArrayList(Collection<? extends E> c)
#ArrayList(Collection<? extends E> c)
构造方法,使用传入的 c
集合,作为 ArrayList 的 elementData
。代码如下:
// ArrayList.java
|
- 比较让人费解的是,在
<X>
处的代码。它是用于解决 JDK-6260652 的 Bug 。它在 JDK9 中被解决,😈 也就是说,JDK8 还会存在该问题。
我们来看一段能够触发 JDK-6260652 的测试代码,然后分别在 JDK8 和 JDK13 下执行。代码如下:
// ArrayListTest.java
|
- JDK8 执行如下图所示:
- JDK13 执行如下图所示:
- 在 JDK8 中,返回的实际是
Integer []
数组,那么我们将 Object 对象设置到其中,肯定是会报错的。具体怎么修复的,看 JDK-6260652 的最末尾一段。
③ #ArrayList()
无参数构造方法 #ArrayList()
构造方法,也是我们使用最多的构造方法。代码如下:
// ArrayList.java
|
- 在我们学习 ArrayList 的时候,一直被灌输了一个概念,在未设置初始化容量时,ArrayList 默认大小为 10 。但是此处,我们可以看到初始化为
DEFAULTCAPACITY_EMPTY_ELEMENTDATA
这个空数组。这是为什么呢?ArrayList 考虑到节省内存,一些使用场景下仅仅是创建了 ArrayList 对象,实际并未使用。所以,ArrayList 优化成初始化是个空数组,在首次添加元素时,才真正初始化为容量为 10 的数组。 - 那么为什么单独声明了
DEFAULTCAPACITY_EMPTY_ELEMENTDATA
空数组,而不直接使用EMPTY_ELEMENTDATA
呢?在下文中,我们会看到DEFAULTCAPACITY_EMPTY_ELEMENTDATA
首次扩容为 10 ,而EMPTY_ELEMENTDATA
按照 1.5 倍扩容从 0 开始而不是 10 。😈 两者的起点不同,嘿嘿。
5. 添加单个元素
#add(E e)
方法,顺序添加单个元素到数组。代码如下:
// ArrayList.java
|
<1>
处,增加数组修改次数modCount
。在父类 AbstractList 上,定义了modCount
属性,用于记录数组修改次数。<2>
处,如果元素添加的位置就超过末尾(数组下标是从 0 开始,而数组大小比最大下标大 1),说明数组容量不够,需要进行扩容,那么就需要调用#grow()
方法,进行扩容。稍后我们在 「6. 数组扩容」 小节来讲。<3>
处,设置到末尾。<4>
处,数量大小加一。
总体流程上来说,抛开扩容功能,和我们日常往 []
数组里添加元素是一样的。
看懂这个方法后,胖友自己来看看 #add(int index, E element)
方法,插入单个元素到指定位置。代码如下:
// ArrayList.java
|
6. 数组扩容
#grow()
方法,扩容数组,并返回它。整个的扩容过程,首先创建一个新的更大的数组,一般是 1.5 倍大小(为什么说是一般呢,稍后我们会看到,会有一些小细节),然后将原数组复制到新数组中,最后返回新数组。代码如下:
// ArrayList.java
|
<1>
处,调用#grow(int minCapacity)
方法,要求扩容后至少比原有大 1 。因为是最小扩容的要求,实际是允许比它大。<2>
处,如果原容量大于 0 时,又或者数组不是 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 时,则计算新的数组大小,并创建扩容。ArraysSupport#newLength(int oldLength, int minGrowth, int prefGrowth)
方法,计算新的数组大小。简单来说,结果就是Math.max(minGrowth, prefGrowth) + oldLength
,按照minGrowth
和prefGrowth
取大的。- 一般情况下,从
oldCapacity >> 1
可以看处,是 1.5 倍扩容。但是会有两个特殊情况:1)初始化数组要求大小为 0 的时候,0 >> 1
时(>> 1 为右移操作,相当于除以 2
)还是 0 ,此时使用minCapacity
传入的 1 。2)在下文中,我们会看到添加多个元素,此时传入的minCapacity
不再仅仅加 1 ,而是扩容到elementData
数组恰好可以添加下多个元素,而该数量可能会超过当前 ArrayList 0.5 倍的容量。
<3>
处,如果是DEFAULTCAPACITY_EMPTY_ELEMENTDATA
数组,直接创建新的数组即可。思考下,如果无参构造方法使用EMPTY_ELEMENTDATA
的话,无法实现该效果了。
既然有数组扩容方法,那么是否有缩容方法呢?在 #trimToSize()
方法中,会创建大小恰好够用的新数组,并将原数组复制到其中。代码如下:
// ArrayList.java
|
同时,提供 #ensureCapacity(int minCapacity)
方法,保证 elementData
数组容量至少有 minCapacity
。代码如下:
// ArrayList.java
|
- 比较简单,我们可以将这个方法理解成主动扩容。
7. 添加多个元素
#addAll(Collection<? extends E> c)
方法,批量添加多个元素。在我们明确知道会添加多个元素时,推荐使用该该方法而不是添加单个元素,避免可能多次扩容。代码如下:
// ArrayList.java
|
<1>
处,如果elementData
剩余的空间不足,则进行扩容。要求扩容的大小,至于能够装下a
数组。当然,在 「6. 数组扩容」 的小节,我们已经看到,如果要求扩容的空间太小,则扩容 1.5 倍。<2>
处,将a
复制到elementData
从s
开始位置。
总的看下来,就是 #add(E e)
方法的批量版本,优势就正如我们在本节开头说的,避免可能多次扩容。
看懂这个方法后,胖友自己来看看 #addAll(int index, Collection<? extends E> c)
方法,从指定位置开始插入多个元素。代码如下:
// ArrayList.java
|
- 重点看【差异点】部分。
8. 移除单个元素
#remove(int index)
方法,移除指定位置的元素,并返回该位置的原元素。代码如下:
// ArrayList.java
|
-
重点是
<X>
处,调用#fastRemove(Object[] es, int i)
方法,快速移除。代码如下// ArrayList.java
private void fastRemove(Object[] es, int i) {
// 增加数组修改次数
modCount++;
// <Y>如果 i 不是移除最末尾的元素,则将 i + 1 位置的数组往前挪
final int newSize;
if ((newSize = size - 1) > i) // -1 的原因是,size 是从 1 开始,而数组下标是从 0 开始。
System.arraycopy(es, i + 1, es, i, newSize - i);
// 将新的末尾置为 null ,帮助 GC
es[size = newSize] = null;
}<Y>
处,看起来比较复杂,胖友按照“如果 i 不是移除最末尾的元素,则将 i + 1 位置的数组往前挪”来理解,就很好懂了。
#remove(Object o)
方法,移除首个为 o
的元素,并返回是否移除到。代码如下:
// ArrayList.java
|
- 和
#remove(int index)
差不多,就是在<Z>
处,改成获得首个为o
的位置,之后就调用#fastRemove(Object[] es, int i)
方法,快速移除即可。
9. 移除多个元素
我们先来看 #removeRange(int fromIndex, int toIndex)
方法,批量移除 [fromIndex, toIndex)
的多个元素,注意不包括 toIndex
的元素噢。代码如下:
// ArrayList.java
|
-
<X>
处,调用#shiftTailOverGap(Object[] es, int lo, int hi)
方法,移除[fromIndex, toIndex)
的多个元素。代码如下:// ArrayList.java
private void shiftTailOverGap(Object[] es, int lo, int hi) {
// 将 es 从 hi 位置开始的元素,移到 lo 位置开始。
System.arraycopy(es, hi, es, lo, size - hi);
// 将从 [size - hi + lo, size) 的元素置空,因为已经被挪到前面了。
for (int to = size, i = (size -= hi - lo); i < to; i++)
es[i] = null;
}- 和
#fastRemove(Object[] es, int i)
方法一样的套路,先挪后置null
。 - 有一点要注意,ArrayList 特别喜欢把多行代码写成一行。所以,可能会有胖又会有疑惑,貌似这里没有修改数组的大小
size
啊?答案在i = (size -= hi - lo)
,简直到精简到难懂。
- 和
#removeAll(Collection<?> c)
方法,批量移除指定的多个元素。实现逻辑比较简单,但是看起来会比较绕。简单来说,通过两个变量 w
(写入位置)和 r
(读取位置),按照 r
顺序遍历数组(elementData
),如果不存在于指定的多个元素中,则写入到 elementData
的 w
位置,然后 w
位置 + 1 ,跳到下一个写入位置。通过这样的方式,实现将不存在 elementData
覆盖写到 w
位置。可能理解起来有点绕,当然看代码也会有点绕绕,嘿嘿。代码如下:
// ArrayList.java
|
-
调用
#batchRemove(Collection<?> c, boolean complement, final int from, final int end)
方法,批量移除指定的多个元素。代码如下:// ArrayList.java
boolean batchRemove(Collection<?> c, boolean complement, final int from, final int end) {
// 校验 c 非 null 。
Objects.requireNonNull(c);
final Object[] es = elementData;
int r;
// Optimize for initial run of survivors
// <1> 优化,顺序遍历 elementData 数组,找到第一个不符合 complement ,然后结束遍历。
for (r = from;; r++) {
// <1.1> 遍历到尾,都没不符合条件的,直接返回 false 。
if (r == end)
return false;
// <1.2> 如果包含结果不符合 complement 时,结束
if (c.contains(es[r]) != complement)
break;
}
// <2> 设置开始写入 w 为 r ,注意不是 r++ 。
// r++ 后,用于读取下一个位置的元素。因为通过上的优化循环,我们已经 es[r] 是不符合条件的。
int w = r++;
try {
// <3> 继续遍历 elementData 数组,如何符合条件,则进行移除
for (Object e; r < end; r++)
if (c.contains(e = es[r]) == complement) // 判断符合条件
es[w++] = e; // 移除的方式,通过将当前值 e 写入到 w 位置,然后 w 跳到下一个位置。
} catch (Throwable ex) {
// Preserve behavioral compatibility with AbstractCollection,
// even if c.contains() throws.
// <4> 如果 contains 方法发生异常,则将 es 从 r 位置的数据写入到 es 从 w 开始的位置
System.arraycopy(es, r, es, w, end - r);
w += end - r;
// 继续抛出异常
throw ex;
} finally { // <5>
// 增加数组修改次数
modCount += end - w;
// 将数组 [w, end) 位置赋值为 null 。
shiftTailOverGap(es, w, end);
}
return true;
}- 不要慌,我们先一起看下每一小块的逻辑。然后,胖友自己调试下,妥妥的就明白了。
complement
参数,翻译过来是“补足”的意思。怎么理解呢?表示如果elementData
元素在c
集合中时,是否保留。- 如果
complement
为false
时,表示在集合中,就不保留,这显然符合#removeAll(Collection<?> c)
方法要移除的意图。 - 如果
complement
为true
时,表示在集合中,就暴露,这符合我们后面会看到的#retainAll(Collection<?> c)
方法要求交集的意图。
- 如果
<1>
处,首先我们要知道这是一个基于 Optimize 优化的目的。我们是希望先判断是否elementData
没有任何一个符合c
的,这样就无需进行执行对应的移除逻辑。但是,我们又希望能够避免重复遍历,于是就有了这样一块的逻辑。总的来说,这块逻辑的目的是,优化,顺序遍历elementData
数组,找到第一个不符合complement
,然后结束遍历。<1.1>
处,遍历到尾,都没不符合条件的,直接返回false
。也就是说,丫根就不需要进行移除的逻辑。<1.2>
处,如果包含结果不符合complement
时,结束循环。可能有点难理解,我们来举个例子。假设elementData
是[1, 2, 3, 1]
时,c
是[2]
时,那么在遍历第 0 个元素1
时,则c.contains(es[r]) != complement => false != false
不符合,所以继续缓存;然后,在遍历第 1 个元素2
时,c.contains(es[r]) != complement => true != false
符合,所以结束循环。此时,我们便找到了第一个需要移除的元素的位置。当然,移除不是在这里执行,我们继续往下看。😈 淡定~
<2>
处,设置开始写入w
为r
,注意不是r++
。这样,我们后续在循环elementData
数组,就会从w
开始写入。并且此时,r
也跳到了下一个位置,这样间接我们可以发现,w
位置的元素已经被“跳过”了。<3>
处,继续遍历elementData
数组,如何符合条件,则进行移除。可能有点难理解,我们继续上述例子。遍历第 2 个元素3
时候,c.contains(es[r]) == complement => false == false
符合,所以将3
写入到w
位置,同时w
指向下一个位置;遍历第三个元素1
时候,c.contains(es[r]) == complement => true == false
不符合,所以不进行任何操作。<4>
处,如果 contains 方法发生异常,则将es
从r
位置的数据写入到es
从w
开始的位置。这样,保证我们剩余未遍历到的元素,能够挪到从从w
开始的位置,避免多出来一些元素。<5>
处,是不是很熟悉,将数组[w, end)
位置赋值为null
。- 还是那句话,如果觉得绕,多调试,可以手绘点图,辅助理解下哈。
#retainAll(Collection<?> c)
方法,求 elementData
数组和指定多个元素的交集。简单来说,恰好和 #removeAll(Collection<?> c)
相反,移除不在 c
中的元素。代码如下:
// ArrayList.java
|
- 试着按照艿艿上面解释的,自己走一波。
10. 查找单个元素
#indexOf(Object o)
方法,查找首个为指定元素的位置。代码如下:
// ArrayList.java
|
而 #contains(Object o)
方法,就是基于该方法实现。代码如下:
// ArrayList.java
|
有时我们需要查找最后一个为指定元素的位置,所以会使用到 #lastIndexOf(Object o)
方法。代码如下:
// ArrayList.java
|
11. 获得指定位置的元素
#get(int index)
方法,获得指定位置的元素。代码如下:
// ArrayList.java
|
- 随机访问
index
位置的元素,时间复杂度为 O(1) 。
12. 设置指定位置的元素
#set(int index, E element)
方法,设置指定位置的元素。代码如下:
// ArrayList.java
|
13. 转换成数组
#toArray()
方法,将 ArrayList 转换成 []
数组。代码如下:
// ArrayList.java
|
- 注意,返回的是
Object[]
类型噢。
实际场景下,我们可能想要指定 T
泛型的数组,那么我们就需要使用到 #toArray(T[] a)
方法。代码如下:
// ArrayList.java
|
- 分成 2 个情况,根据传入的
a
数组是否足够大。 <1>
处,如果传入的数组小于size
大小,则直接复制一个新数组返回。一般情况下,我们不会这么干。<2>
处,将elementData
复制到a
中。<2.1>
处,如果传入的数组大于size
大小,则将size
位置赋值为null
。额,有点没搞懂这个有啥目的。<2.2>
处,返回传入的a
。很稳。
- 考虑到
<1>
处,可能会返回一个新数组,所以即使<2>
返回的就是a
数组,最好使用还是按照a = list.toArray(a)
。
14. 求哈希值
#hashCode()
方法,求 ArrayList 的哈希值。代码如下:
// ArrayList.java
|
- 可能胖友会好奇,为什么使用 31 作为乘子呢?可以看看 《科普:为什么 String hashCode 方法选择数字 31 作为乘子》 。
15. 判断相等
#equals(Object o)
方法,判断是否相等。代码如下:
// ArrayList.java
|
- 可能第一眼让胖友比较费解的是,为什么根据类型是否为 ArrayList ,调用了两个不同的方法去比对呢?因为普通的 List ,我们只能使用 Iterator 进行迭代,相比 ArrayList 的
elementData
属性遍历,性能会略低一些。😈 处处是细节哈。 -
这两个方法的代码如下,已经添加详细注释。代码如下:
// ArrayList.java
boolean equalsRange(List<?> other, int from, int to) {
// 如果 to 大于 es 大小,说明说明发生改变,抛出 ConcurrentModificationException 异常
final Object[] es = elementData;
if (to > es.length) {
throw new ConcurrentModificationException();
}
// 通过迭代器遍历 other ,然后逐个元素对比
var oit = other.iterator();
for (; from < to; from++) {
// 如果 oit 没有下一个,或者元素不相等,返回 false 不匹配
if (!oit.hasNext() || !Objects.equals(es[from], oit.next())) {
return false;
}
}
// 通过 oit 是否遍历完。实现大小是否相等的效果。
return !oit.hasNext();
}
private boolean equalsArrayList(ArrayList<?> other) {
// 获得 other 数组修改次数
final int otherModCount = other.modCount;
final int s = size;
boolean equal;
// 判断数组大小是否相等
if (equal = (s == other.size)) {
final Object[] otherEs = other.elementData;
final Object[] es = elementData;
// 如果 s 大于 es 或者 otherEs 的长度,说明发生改变,抛出 ConcurrentModificationException 异常
if (s > es.length || s > otherEs.length) {
throw new ConcurrentModificationException();
}
// 遍历,逐个比较每个元素是否相等
for (int i = 0; i < s; i++) {
if (!Objects.equals(es[i], otherEs[i])) {
equal = false;
break; // 如果不相等,则 break
}
}
}
// 如果 other 修改次数发生改变,则抛出 ConcurrentModificationException 异常
other.checkForComodification(otherModCount);
return equal;
}
16. 清空数组
#clear()
方法,清空数组。代码如下:
// ArrayList.java
|
17. 序列化数组
#writeObject(java.io.ObjectOutputStream s)
方法,实现 ArrayList 的序列化。代码如下:
// ArrayList.java
|
<1>
处,调用ObjectOutputStream#defaultWriteObject()
方法,写入非静态属性、非transient
属性。可能有些胖友不了解 Java 的序列化相关的知识,可以看看 《Serializable 原理》 文章。<2>
处,写入size
,主要为了与 clone 方法的兼容。不过艿艿也觉得挺奇怪的,明明在<1>
处,已经写入了size
,这里怎么还来这么一出呢?各种翻查资料,暂时只看到 《源码分析:ArrayList 的 writeobject 方法中的实现是否多此一举?》 有个讨论。<3>
吹,逐个写入elementData
元素的数组。我们回过来看下elementData
的定义,它是一个transient
修饰的属性。为什么呢?因为elementData
数组,并不一定是全满的,而可能是扩容的时候有一定的预留,如果直接序列化,会有很多空间的浪费,所以只序列化从[0, size)
的元素,减少空间的占用。
18. 反序列化数组
#readObject(java.io.ObjectInputStream s)
方法,反序列化数组。代码如下:
// ArrayList.java
|
- 和序列化的过程,恰好相反(哈哈哈,不然还想咋样),一眼就看的明白。
19. 克隆
#clone()
方法,克隆 ArrayList 对象。代码如下:
// ArrayList.java
|
- 注意,
elementData
是重新拷贝出来的新的数组,避免和原数组共享。
20. 创建子数组
#subList(int fromIndex, int toIndex)
方法,创建 ArrayList 的子数组。代码如下:
// ArrayList.java
|
- 实际使用时,一定要注意,SubList 不是一个只读数组,而是和根数组
root
共享相同的elementData
数组,只是说限制了[fromIndex, toIndex)
的范围。 - 这块的源码,并不复杂,所以这里也就不展开了。一般情况下,我们也不需要了解它的源码,嘿嘿。
21. 创建 Iterator 迭代器
#iterator()
方法,创建迭代器。一般情况下,我们使用迭代器遍历 ArrayList、LinkedList 等等 List 的实现类。代码如下:
// ArrayList.java
|
- 创建 Itr 迭代器。Itr 实现
java.util.Iterator
接口,是 ArrayList 的内部类。虽然说 AbstractList 也提供了一个 Itr 的实现,但是 ArrayList 为了更好的性能,所以自己实现了,在其类上也有注释“An optimized version of AbstractList.Itr”。
Itr 一共有 3 个属性,如下:
// ArrayList.java#Itr
|
- 每个属性,胖友自己看看注释噢。
下面,让我们来看看 Itr 对 Iterator 的 4 个实现方法。
#hasNext()
方法,判断是否还可以继续迭代。代码如下:
// ArrayList.java#Itr
|
cursor
如果等于size
,说明已经到数组末尾,无法继续迭代了。
#next()
方法,下一个元素。代码如下:
// ArrayList.java#Itr
|
<1>
处,记录当前cursor
的位置。因为我们当前返回的就是要求cursor
位置的元素。<2>
处,cursor
指向下一个位置。<3>
处,返回当前位置的元素。同时在<4>
处,会将lastRet
指向当前位置。
#remove()
方法,移除当前元素。代码如下:
// ArrayList.java#Itr
|
<1>
处,调用#remove(int index)
方法,移除lastRet
位置的元素。所以,如果要注意,如果移除元素比较前面,会将后面位置的往前挪,即复制,可能比较消耗性能。<2>
处,cursor
指向lastRet
位置,因为被移了,所以需要后退下。<3>
处,lastRet
标记为-1
,因为当前元素被移除了。<4>
处,记录新的数组的修改次数。因为此处修改了数组,如果不修改下,后续迭代肯定会报错。
#forEachRemaining(Consumer<? super E> action)
方法,消费剩余未迭代的元素。代码如下:
// ArrayList.java#Itr
|
- 比较简单,胖友自己瞅瞅。貌似平时这个方法用的不是很多。
22. 创建 ListIterator 迭代器
艿艿:可能一些胖友不了解 ListIterator 迭代器,因为平时使用不多。可以先去看看 《Java 集合框架之 Iterator 和 ListIterator》 。简单来说,ListIterator 是为 List 设计的,功能更强大的 Iterator 迭代器。
#listIterator(...)
方法,创建 ListIterator 迭代器。代码如下:
// ArrayList.java
|
- 创建 ListItr 迭代器。ListItr 实现
java.util.ListIterator
接口,是 ArrayList 的内部类。虽然说 AbstractList 也提供了一个 ListItr 的实现,但是 ArrayList 为了更好的性能,所以自己实现了,在其类上也有注释“An optimized version of AbstractList.ListItr”。
ListItr 直接继承 Itr 类,无自定义的属性。代码如下:
// ArrayList.java#ListItr
|
- 可以手动设置指定的位置开始迭代。
因为 ListItr 的实现代码比较简单,我们就不逐个来看了,直接贴加了注释的代码。代码如下:
// ArrayList.java#ListItr
|
666. 彩蛋
咳咳咳,比想象中的长的多的一篇文章。并且实际上,我们还有几个 ArrayList 的方法的解析没有写,如下:
#spliterator()
#removeIf(Predicate<? super E> filter)
#replaceAll(UnaryOperator<E> operator)
#sort(Comparator<? super E> c)
#forEach(Consumer<? super E> action)
哈哈,也是比较简单的方法,胖友自己可以解决一波的哈。就当,课后作业?!嘿嘿。
下面,我们来对 ArrayList 做一个简单的小结:
- ArrayList 是基于
[]
数组实现的 List 实现类,支持在数组容量不够时,一般按照 1.5 倍自动扩容。同时,它支持手动扩容、手动缩容。 -
ArrayList 随机访问时间复杂度是 O(1) ,查找指定元素的平均时间复杂度是 O(n) 。
可能胖友对时间复杂度的计算方式不是很了解,可以看看 《算法复杂度分析(上):分析算法运行时,时间资源及空间资源的消耗》 和 《算法复杂度分析(下):最好、最坏、平均、均摊等时间复杂度概述》 两文。
-
ArrayList 移除指定位置的元素的最好时间复杂度是 O(1) ,最坏时间复杂度是 O(n) ,平均时间复杂度是 O(n) 。
最好时间复杂度发生在末尾移除的情况。
-
ArrayList 移除指定元素的时间复杂度是 O(n) 。
因为首先需要进行查询,然后在使用移除指定位置的元素,无论怎么计算,都需要 O(n) 的时间复杂度。
-
ArrayList 添加元素的最好时间复杂度是 O(1) ,最坏时间复杂度是 O(n) ,平均时间复杂度是 O(n) 。
最好时间复杂度发生在末尾添加的情况。
结尾在抛个拓展,在 Redis String 的数据结构,实现方式是类似 Java ArrayList 的方式,感兴趣的胖友可以自己去瞅瞅。