一家之言 姑妄言之 絮絮叨叨 不足为训
笔者废话:
这篇文章是ArrayList源码逐条解析外述篇。为什么来个外述篇呢?因为:
1. 这个类作为ArrayList
的迭代方式是非常重要的;
2. 我是实在不想在“ArrayList的遍历功能解析”中解析这个类了,本身它是非常重要的,如果不单拿出来讲而是放在ArrayList源码逐条解析这个文章里解析其实会给人造成误解认为其不重要;
3. 公司里的领导告诉我源码分析写的过于长可能影响观感。
所以,我这里把这个类单拿出来进行解析(>ω<)。
ArrayList-Itr类注释翻译:
一个优化版的AbstractList.Itr
笔者废话:
这个注释其实已经告诉了我们,这个类是一个优化类,是我们ArrayList
所继承的AbstractList
抽象类中Itr
类的一个优化版本。我们平常在使用ArrayList
的iterator()
方法时,其底层用的就是这个类。
ArrayList-Itr成员变量信息:
int cursor; // 下一个要返回的元素的索引
int lastRet = -1; // 最后一个返回元素的索引;如果没有就是-1
int expectedModCount = modCount;
我们来看ArrayList-Itr的成员变量信息。
首先,cursor
代表了下一个要返回的元素的索引,这里源码的单行注释也已经说的很通透了。其实cursor
就是一个游标指向,指向的就是我们所将要遍历数组中的下一项。譬如我们有一个数组:[“a” , “b” , “c” , “d”]。
当我们初始化拿到Iterator
的时候,这个cursor
就已经指向了第一项,也就是说cursor = 0
,代表,你好,我接下来要开始指向"a"了。
其次,lastRet
代表了最后一个返回元素的索引,如果没有就是-1。这里源码的单行注释也已经说的很通透了。其实lastRet
也是一个指向,指向的就是我们所将要遍历数组中的上一项。我们还拿上面例子的数组举例。
当我们初始化拿到Iterator
的时候,这个cursor
就已经指向了第一项,也就是说cursor = 0
,那么lastRet
指向哪里呢?答,指向上一项。也就是元素"a"的左侧,也就是"没有",那么"没有"是多少呢?看注释,-1。
所以这里我们就可以进行推算了,当cursor
已经指向了第二项,也就是指向了"b",这个时候cursor = 1
,而lastRet
指向上一项,也就是lastRet = 0
。每一次lastRet
都会比cursor
少1,cursor
表示当前元素,lastRet
则表示上一个元素。
最后,我们来看expectedModCount
。从字面翻译上来看叫做“期望修改值”,那么这个“期望修改值”是谁呢。代码中告诉我们这个值就是我们ArrayList
中的操作值modCount
。其实这个modCount
都是继承自AbstractList
抽象类中的,他们是互通的。注意:这个属性很重要,这里会引出ConcurrentModificationException这个异常的触发场景。
笔者废话:
一般人们喜欢将cursor
其翻译成“指针”,我是觉得这种翻译…可能这种翻译更加的直白,但是我还是喜欢把它翻译成“游标”。因为我们的Java当初宣称是程序员不需要操作指针,而且Java也屏蔽(封装在底层)指针,如果这里依旧用“指针”这个字眼我个人认为不是很合适。当然,我特别喜欢“游标”这个词。因为这让我想起了我最初用的游标卡尺。仿佛将这个游标卡尺比喻成我们ArrayList
所代表的底层数组再也合适不过了。
ArrayList-Itr构造函数信息:
Itr() {}
这里是Itr
的构造函数,从形式上来看是一个无参构造函数。这里显式声明无参构造函数的目的是为了Itr
的子类ListItr
在进行构造时能搜索到父类的构造函数,这个从ListItr
的有参构造函数就可能观察得到。另外,这个无参构造函数也在被ArrayList
的iterator()
所调用,具体就不解释了,都在ArrayList源码逐条解析里了。
ArrayList-Itr的方法解析:
好,我们现在开始进入正题了,别忘了,这个Itr
类就是对接口Iterator
的具体实现。具体的情形可先阅读Iterator源码逐条解析倒数第二段。
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
这里我们先来介绍一个方法checkForComodification()
——检测修改值方法。我
们先不关切这个方法被谁来调用,我们就单纯的看它在干什么。
首先,我们看到这个方法被final
关键字修饰了,这代表如果这个类被继承的话,这个方法是不可以被重写的。
其次,这里面的代码体也非常简单,它在判断,我们当前ArrayList
的操作值modCount
是否不等于Irt
的期望修改值expectedModCount
。一旦发现不相等,就抛出ConcurrentModificationException
异常。
OK,这里,我们就把本文“ArrayList-Itr成员变量信息”中所介绍的ConcurrentModificationException异常触发场景的坑填了。
也就是说,一旦我发现你当前的修改值并不等于我当初的修改值,那么,你完了,你已经违反了我们ArrayList
的规定:对当前数组内的内容进行了修改,无论是单线程还是多线程。这个时候是必然抛出ConcurrentModificationException
异常的。
它就是判断程序对当前数组遍历过程中是否有非法操作行为进行合理化判断,避免内部元素更改造成业务逻辑混乱(也对,你遍历就遍历你瞎改什么…)。
public boolean hasNext() {
return cursor != size;
}
这里覆写了Iterator
接口中的hasNext()
。我们知道这个方法是在询问当前遍历的容器中是否含有下一个元素,那么我们看具体的实现是如何呢?
cursor != size
,对,就是这样。它在判断我们的cursor
游标是否不等于当前数组内元素的个数size
。因为如果等于了size
不就代表我们这个游标指向已经指向了最后一个元素的后一位了吗?
这里要记住,我们的游标是从0
这个位置开始的,所以即使遍历到了最后一个元素它的值也是size-1
。只有执行了next()
方法之后,利用next()
方法将元素取出后,这个cursor
才会+1(也就是游标指向向右移动),达到size
这个值的水平。这时也能得出我们已经遍历了最后一个元素,不会有下一个元素了。所以我们说,这里的实现方式非常的精妙,利用一个游标就定位出是否含有下一个元素。
千万记住,这里的hasNext()
方法调用完后,游标指向cursor
是不会移动的。
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
这里是迭代的重要步骤,这个next()才是真正可以取出元素的方法。我们经过了之前的hasNext()
方法判断是否含有下一个元素之后,就可以紧接着调用该方法。
我们来看这个方法的具体实现。
第一步,先利用checkForComodification()
方法对修改值modCount
进行检查,判断当前数组是否发生修改。
第二步,声明一个局部变量i
,并将当前游标值cursor
赋予它(其实这里就是游标本身,我们接下来描述的时候就用“游标”这个概念)。
第三步,判断游标是否大于或等于当前数组元素个数size
,如果符合判断条件,则抛出NoSuchElementException
异常。这点还是易于理解的,之前我们说过,当这个游标cursor
的值等于了数组元素个数size
就已经表示没有剩余的集合进行遍历了,何况还要大于size
。那么这个时候抛出异常就不足为奇了。
第四步,是将当前ArrayList
的数组赋予一个新的类型为Object
的数组,其实就是把当前的数组复制了一份;
第五步,到了这里,你看,又一次把游标进行判断,判断什么呢?判断我们的游标是否大于或等于了数组的容量length
,如果符合判断条件,则抛出ConcurrentModificationException
异常。看,我们这里又有一个熟悉的异常,那么为什么游标大于或等于了数组的容量length
就会抛出这个异常呢?其实,它还是在做检测,检测集合是否被修改过。
笔者废话:
到这里你仔细想想,我们假设元素个数size
就是数组长度length
,也就是说这个数组已经被填满了。但是这个时候,你通过了第三步的元素个数判断,但是没有通过第五步的容量判断,也就是发生了这种场景:length <= i < size
。这种情况可能吗?数组容量小于了数组内的元素个数?这种情况有可能,就是你删除了元素,然后你的数组缩容了。说的直白一些当你remove()
完毕后你又调用了类似于trimToSize()
的缩容方法。这可不就是修改了集合元素嘛~所以,这个时候抛出ConcurrentModificationException
异常是一种非常正确的行为。
第六步,这个时候,该判断的也都判断了,我们可以正常的取出元素返回给调用者了。不过这里我们需要开始最重要的一步了,挪动我们的游标卡尺。形如代码所说的那样,当前游标i + 1
即可。也就是这个游标指向向右移动一格。
第七步,返回所遍历的元素,返回哪个呢?当然返回我们之前想要遍历的那个游标所在处的元素,也就是那个i
,也就是返回数组当前i
索引处的元素。不过,返回之前我们还要明白,既然我们的游标cursor
已经向右挪动了一格,那么是不是我们的上一项游标lastRet
也要向右挪动一格呢?它肯定是紧跟cursor
的,所以,这个lastRet
值就是当前的i
值。不用怀疑,因为你的下一项游标cursor
已经加1了。这个cursor
的左侧lastRet
势必就是当初的那个小i
,再细致一点的话就是cursor = i + 1
,lastRet = cursor - 1 = i + 1 - 1 = i
。
到此,next()
方法解析完成~
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
接下来我们介绍一个移除方法——remove()
。
这个方法主要是删除利用next()
方法返回的元素。那么这里为什么另起一个移除方法,而不是用ArrayList
本身的remove()
方法呢?我们往下看…
我们这里先不介绍代码的具体步骤,先举出最核心的移除步骤,也就是ArrayList.this.remove(lastRet)
。我们看,其实整个移除方法利用的还是ArrayList
内部的remove()
方法。当然,这里不是重点,重点是那个索引值——lastRet
。记住这个值,这里删除的是当前游标所在位置左移一格的元素(当前一项的前一项)。根据上述next()
方法解析来看,当我们元素获取之后,游标cursor
右移,同时上一项游标lastRet
也进行右移。所以,这个时候lastRet
指向的就是我们之前next()
方法遍历出的那个元素。这也就解释了为什么我们一般使用这个remove()
方法的时候是跟在next()
方法之后的(因为你想删除的那个元素就是lastRet
指向的那个元素)。
所以这个时候,我们才开始代码的解析步骤:
第一步,判断lastRet
是否小于0
。如果小于了0
,则抛出IllegalStateException
异常。
我们之前说过,当我们初始化拿到Iterator
的时候,这个cursor
就已经指向了第一项,也就是说cursor = 0
,而lastRet
是在起始位置的左侧,也就是"没有",也就是-1。那么我们上面说了,调用remove()
方法删除的就是lastRet
指向的那个元素,然而这个的索引值是-1
,我们能删除一个不存在的元素吗?显然是不能的。只有在调用一次next()
方法之后游标右移,才可以正常的删除元素。所以,这里需要进行一次判断,来规避错误删除的场景。
第二步,利用checkForComodification()
方法对修改值modCount
进行检查,判断当前数组是否发生修改。这一步属于通用操作,就不须解释了。
第三步,构建try-catch
代码块,一旦捕获到异常就抛出ConcurrentModificationException
异常(其实这里还是围绕操作值modCount
和预期修改值exceptModCount
展开描述的)。
第四步,极为重要的一步——移除元素。虽说上面已经提到这个方法了,我这里还是提一下。这一步会调用ArrayList
内部的remove(int index)
方法,注意,这里的remove(int index)
方法可是会修改操作值modCount
的。
第五步,将当前的lastRet
赋予当前的cursor
。这一步其实就是我们之前在ArrayList源码逐条解析中讲到的remove(int index)
填坑操作。当我们把当前的lastRet
所指向的元素删除时,这里其实就已经有了空缺,但是放心,remove(lastRet)
操作通过复制数组已经把这个元素的后一项元素直到最后一项元素往前移动了。所以,我们之前通过next()
操作移动的游标cursor
其实已经指向了其本身元素的后一项。因此,这里就需要把游标cursor
前移一格,也就是往左移,怎么移动呢?不就是lastRet
所代表的那个位置吗?也就是说这里会有一步cursor = lastRet
的操作让游标cursor
指向正确的地方。
可能这种表述不太直观,这里还是举个例子:譬如我们有一个数组:
a | b | c | d | e |
---|---|---|---|---|
0 | 1 | 2 | 3 | 4 |
当我们遍历到"b"的时候,cursor = 2
,lastRet = 1
。这个时候我们把"b"删除,这个数组就变成了:
a | c | d | e |
---|---|---|---|
0 | 1 | 2 | 3 |
那么这个时候,如果是cursor = 2
,lastRet = 1
就肯定不正确了。cursor
指向了"d"而跳过了"c",我们需要把这个游标归正回来,所以就使用cursor = lastRet
让cursor = 1
以使cursor
指向"c"。这样就清楚多了。
第六步,将lastRet
初始化为最初的-1
。其实这一步我并不明白为什么会初始化为-1
,我想着它可以做这种操作:lastRet = lastRet - 1
。也就是这个最后一个返回元素的索引向左移动一格。不过,这种初始化-1的操作也不影响程序的正常运行,因为在next()
方法中,这个lastRet
值依旧会被赋予正确的指向。
第七步,同步操作值modCount
到exceptModCount
,这个就不须过多的解释了,因为remove(lastRet)
操作会使得当前ArrayList
的操作值modCount
加1,所以这里为了避免ConcurrentModificationException
异常你需要把这个修改了的值更新到我们Itr
中的期望修改值exceptModCount
内。
到此,remove()
方法解析完成~
@Override
@SuppressWarnings("unchecked")
public void forEachRemaining(Consumer<? super E> consumer) {
Objects.requireNonNull(consumer);
final int size = ArrayList.this.size;
int i = cursor;
if (i >= size) {
return;
}
final Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length) {
throw new ConcurrentModificationException();
}
while (i != size && modCount == expectedModCount) {
consumer.accept((E) elementData[i++]);
}
// 在迭代结束时更新一次,以减少堆写流量
cursor = i;
lastRet = i - 1;
checkForComodification();
}
我们接下来介绍的这个方法forEachRemaining(Consumer<? super E> consumer)
从方法名来看是在进行遍历操作,遍历什么呢?遍历剩余元素。
笔者废话:
其实这个方法我并没有使用过,但是我还是尝试的做了一些小实验,发现这个方法还是挺可爱的。
这个方法的目的是为了遍历每个剩余元素,然后执行给定的操作,直到所有的元素都已经被处理或抛出一个异常。那么从何而来的这些剩余元素呢?当然是我们之前遍历过后剩余下来的元素。不过,这里需要仔细想一下,什么时候会出现遍历集合还剩余元素的场景呢?
OK,我们这里先打住,这些使用场景我会在后面描述,我们还是先进入主题,进入源码分析部分。
开始分析:
第一步,我们先看方法参数。这个参数是一个Consumer<? super E>
类型的参数。我们仔细跟踪它的话会发现这是一个函数式编程接口,关于这个接口的信息已经在Consumer源码逐条解析中讲解到了,这里就不赘述了。总的来说,从这一步我们就明白了,这个遍历方法是要进行业务处理的。也就是我们之前说过的“执行给定的操作”。
第二步,判断consumer
参数是否为空。确实是,我们需要判断一下给定操作是否存在,不存在的话也没必要往下执行了。
第三步,获取当前ArrayList
的元素个数size
并将其赋予新声明的变量size
。值得一提的是,这里新声明的size
是final
修饰的。代表着这个属性在当前调用的情况下是不可变的。为什么这里是不可变得呢?事实上,final
修饰的变量并非指的是“值”不可变,而是“引用”不可变。我们试想一种情况,假如我们当前的ArrayList
内的元素个数发生了改变,我们还需要用原先的元素个数size
进行遍历吗?显示不行,所以,这里需要“引用不可变”,一直指向这个ArrayList.this.size
。
第四步,我们需要获取当前剩余元素的游标值cursor
并赋予i
,这一步是必须的。
第五步,对这个游标值i
进行判断,如果大于或等于了元素个数size
就返回不执行了。这里大于的情况就不说了,我们着重说一下这个等于的情况。
在第八步的时候我们会真正的对这个集合开始遍历,你会发现取用元素的时候这个游标i
会进行i++
操作。记住这一步,我们一直说数组索引是从0开始的,所以这里遍历完所有元素后最后的那个索引应该是size - 1
。然而这里为了方便游标i
向右移动,所以增添了i++
操作。这种操作是无可厚非的,但是关键在于最后一个元素遍历后,这个游标i
依旧会自动加1。所以这里我们可以理解为此时自增后的i
值不仅仅表示了索引,还表示了元素个数。一旦发生i == size
的情况也就表明了我们整个数组已经遍历完毕,不需要进行再次遍历了。所以,这里就进行了i >= size
判断。一旦符合条件,就返回不执行了。
第六步,获取当前ArrayList
的数组并赋予新声明的elementData
,同时,这个新声明的elementData
也是被final
修饰的。这也表明了我们此时操作的数组引用是不可变的。
第七步,对我们当前的游标i
进行判断,看其是否大于或等于了数组的容量。如果符合判断条件,则抛出ConcurrentModificationException
异常。这个判断解析已经在上面的next()
方法解析中已经讲解过了,如果忘了就跳到上面next()
方法解析的“笔者废话”中再看一遍。
第八步,开始真正的遍历集合,并执行给定的操作。这里的判断依据是我们的游标i
并不等于元素个数size
。也就是说这里要判断我们的元素并没有真正的遍历完。然后同时需要满足的条件是当前ArrayList
的操作值modCount
是符合预期修改值expectedModCount
的,也就是遍历期间是不允许修改集合的。
通过判断之后,就可以取出元素,然后利用消费者consumer
来执行给定的操作。注意,这里取出元素的索引用的就是我们当前的游标i
,只不过操作完成后会执行i++
操作以便移动游标位置(索引位置)。
第九步,这里就是更新现在游标cursor
的操作。将我们现在经过自增的i
赋予cursor
,理论上来看,这个时候的cursor
也代表着我们元素的个数size
(因为我们最后执行的是size - 1 + 1
)。
第十步,更新lastRet
,这个lastRet
的计算结果就比较符合我们之前的那个remove()
方法中第六步所提到的计算方式,即,lastRet = i - 1
。也就是说此时的lastRet
绝对是紧跟在cursor
左侧的。而此时的lastRet
也指向了我们数组中的最后一个元素。
最后,依旧利用checkForComodification()
方法对修改值modCount
进行检查,判断当前数组是否发生修改。
至此,forEachRemaining(Consumer<? super E> consumer)
方法我们就解析完毕了。
不过到这里我们还是对这个方法有些说明的,也是为了填上面什么时候会出现遍历集合还剩余元素的场景的坑。
那么我们可以仔细想一下,这个方法的应用场景是什么时候呢?
其实它的使用场景有两个:
1. 遍历时发生异常,还存有未遍历的元素;
2. 单纯需要用遍历处理业务。
针对于第一条来说,当遍历出现异常的时会进入这个遍历的场景。注意:这里说的遍历是利用iterator()
方法,你用for循环出现了异常这个forEachRemaining(Consumer<? super E> consumer)
是不起作用的。
譬如这个例子:
/**
* 这个是错误示例啊,不过放心运行
*/
public static void main(String[] args) {
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add("a");
arrayList.add("b");
arrayList.add("c");
arrayList.add("d");
Iterator<String> iterator = arrayList.iterator();
try {
for (String a : arrayList) {
if ("a".equals(a)) throw new RuntimeException();
}
} catch (Exception e) {
iterator.forEachRemaining(wangcai -> {
System.out.println("遍历出的剩余元素: " + wangcai);
});
}
}
/*
* 输出结果:
* 遍历出的剩余元素: a
* 遍历出的剩余元素: b
* 遍历出的剩余元素: c
* 遍历出的剩余元素: d
*/
你看这个例子,我们利用for循环抛出了异常,再用forEachRemaining(Consumer<? super E> consumer)
方法进行遍历其实是没变化的。哪有什么剩余元素,全剩下来了。所以,这种操作是错误的。我们来举两个正确使用该方法的例子:
/**
* 这个是正确的示例,放心运行
*/
public static void main(String[] args) {
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add("a");
arrayList.add("b");
arrayList.add("c");
arrayList.add("d");
Iterator<String> iterator = arrayList.iterator();
try {
iterator.forEachRemaining(s -> {
if (iterator.next().equals("a")) throw new RuntimeException();
});
} catch (Exception e) {
iterator.forEachRemaining(laifu -> {
System.out.println("遍历出的剩余元素: " + laifu);
});
}
}
/*
* 输出结果:
* 遍历出的剩余元素: b
* 遍历出的剩余元素: c
* 遍历出的剩余元素: d
*/
/**
* 这个是正确的示例,放心运行
*/
public static void main(String[] args) {
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add("a");
arrayList.add("b");
arrayList.add("c");
arrayList.add("d");
Iterator<String> iterator = arrayList.iterator();
try {
while (iterator.hasNext()) {
if ("a".equals(iterator.next())) throw new RuntimeException();
}
} catch (Exception e) {
iterator.forEachRemaining(laifu -> {
System.out.println("遍历出的剩余元素: " + laifu);
});
}
}
/*
* 输出结果:
* 遍历出的剩余元素: b
* 遍历出的剩余元素: c
* 遍历出的剩余元素: d
*/
你看,我们这里又举了两个例子来证明这个方法的使用场景。就是一旦发生异常,我还可以遍历剩下的元素。无论你是用传统的while
式方法抛出异常,还是利用forEachRemaining(Consumer<? super E> consumer)
方法本身抛出异常,我们都能接住然后进行后续的处理。
比如我们平时推送数据,需要遍历集合推送集合内的数据到其他平台,如果遍历发生异常难道我们就不管了吗?不能吧?所以可以利用这种方式进行保底推送。也就是说,我最少可以规避一次错误,然后将剩余数据推送过去。当然,你第二次推送再发生问题的话,记得报告你的项目经理,商量一下扣工资的事儿,可能你的项目经理会把这个事儿拦下了然后不用扣了的说→_→
针对于第二条来说,你会发现,这其实就是一个遍历方法。即使是我们分析的底层源码也是一个while
循环遍历。唯独的区别就是我们可以执行给定的操作。而且方便的是,因为函数式编程接口的引用,你还可以使用lambda
表达式。毫不避讳的说,够咱们装一壶得了(*•̀ᴗ•́*)و …
所以这么说来,我们是可以利用它做业务处理的(当然第一条也是做了业务处理,这里涉及的可能是重要的业务处理,不过我不推荐这个场景的做法。毕竟,这个方法的本意是要遍历剩余元素)。来,我们举个例子:
/**
* 处理大量业务的使用场景,不过我不太建议这么做,即使咱们说实话这样没什么错
*/
public static void main(String[] args) {
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add("a");
arrayList.add("b");
arrayList.add("c");
arrayList.add("d");
Iterator<String> iterator = arrayList.iterator();
iterator.forEachRemaining(laifu -> {
System.out.println("————————————————————");
System.out.println("你看我能不能推送: " + laifu);
System.out.println("你看我能不能结账: " + laifu);
System.out.println("你看我能不能停单: " + laifu);
System.out.println("你看我能不能吃饭: " + laifu);
System.out.println("————————————————————");
});
}
/*
* 输出结果:
* ————————————————————
* 你看我能不能推送: a
* 你看我能不能结账: a
* 你看我能不能停单: a
* 你看我能不能吃饭: a
* ————————————————————
* ————————————————————
* 你看我能不能推送: b
* 你看我能不能结账: b
* 你看我能不能停单: b
* 你看我能不能吃饭: b
* ————————————————————
* ————————————————————
* 你看我能不能推送: c
* 你看我能不能结账: c
* 你看我能不能停单: c
* 你看我能不能吃饭: c
* ————————————————————
* ————————————————————
* 你看我能不能推送: d
* 你看我能不能结账: d
* 你看我能不能停单: d
* 你看我能不能吃饭: d
* ————————————————————
*/
这个例子不太长就是输出结果长点儿,不过长点儿好,这也能说明这个forEachRemaining(Consumer<? super E> consumer)
方法是不是也可以在正常情况下遍历元素,然后进行大量的业务逻辑处理?对,毋庸置疑,是的。所以…嗯,两个场景介绍完了,坑填平~
至此,我们ArrayList-Itr
到此全部解析完毕(ಥ_ಥ)。