目录
iterator
itr.hasNext 和 itr.next 实现
倒数第二个元素的特殊
如何避坑
都说 ArrayList 在用 foreach 循环的时候, 不能 add 元素, 也不能 remove 元素, 可能会抛异常, 那我们就来分析一下它具体的实现. 我目前的环境是 Java8.
有下面一段代码:
- public class TestForEachList extends BaseTests {
- @Test
- public void testForeach() { List<String> list = new ArrayList<>();
- list.add("1");
- list.add("2");
- list.add("3");
- for (String s : list) {
- }
- }
- }
代码很简单, 一个 ArrayList 添加 3 个元素, foreach 循环一下, 啥都不干. 那么 foreach 到底是怎么实现的呢, 暴力的方法看一下, 编译改类, 用 javap -c TestForEachList 查看 class 文件的字节码, 如下:
- javap -c TestForEachList
- Warning: Binary file TestForEachList contains collection.list.TestForEachList
- Compiled from "TestForEachList.java"
- public class collection.list.TestForEachList extends com.ferret.BaseTests {
- public collection.list.TestForEachList();
- Code:
- 0: aload_0
- 1: invokespecial #1 // Method com/ferret/BaseTests."<init>":()V
- 4: return
- public void testForeach();
- Code:
- 0: new #2 // class java/util/ArrayList
- 3: dup
- 4: invokespecial #3 // Method java/util/ArrayList."<init>":()V
- 7: astore_1
- 8: aload_1
- 9: ldc #4 // String 1
- 11: invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
- 16: pop
- 17: aload_1
- 18: ldc #6 // String 2
- 20: invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
- 25: pop
- 26: aload_1
- 27: ldc #7 // String 3
- 29: invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
- 34: pop
- 35: aload_1
- 36: invokeinterface #8, 1 // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator;
- 41: astore_2
- 42: aload_2
- 43: invokeinterface #9, 1 // InterfaceMethod java/util/Iterator.hasNext:()Z
- 48: ifeq 64
- 51: aload_2
- 52: invokeinterface #10, 1 // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
- 57: checkcast #11 // class java/lang/String
- 60: astore_3
- 61: goto 42
- 64: return
- }
可以勉强读, 大约是调用了 List.iterator, 然后根据 iterator 的 hasNext 方法返回结果判断是否有下一个, 根据 next 方法取到下一个元素.
但是是总归是体验不好, 我们是现代人, 所以用一些现代化的手段, 直接用 idea 打开该 class 文件自动反编译, 得到如下内容:
- public class TestForEachList extends BaseTests {
- public TestForEachList() {
- }
- @Test
- public void testForeach() {
- List<String> list = new ArrayList();
- list.add("1");
- list.add("2");
- list.add("3");
- String var3;
- for(Iterator var2 = list.iterator(); var2.hasNext(); var3 = (String)var2.next()) {
- ;
- }
- }
- }
体验好多了, 再对比上面的字节码文件, 没错
- for(Iterator var2 = list.iterator(); var2.hasNext(); var3 = (String)var2.next()) {
- ;
- }
这就是脱掉语法糖外壳的 foreach 的真正实现.
接下来我们看看这三个方法具体都是怎么实现的:
iterator
ArrayList 的 iterator 实现如下:
- public Iterator<E> iterator() {
- return new Itr();
- }
- private class Itr implements Iterator<E> {
- int cursor; // index of next element to return
- int lastRet = -1; // index of last element returned; -1 if no such
- int expectedModCount = modCount;
- // 省略部分实现
- }
Itr 是 ArrayList 中的内部类, 所以 list.iterator()的作用是返回了一个 Itr 对象赋值到 var2, 后面调用 var2.hasNext(),var2.next()就是 Itr 的具体实现了.
这里还值的一提的是 expectedModCount, 这个变量记录被赋值为 modCount, modCount 是 ArrayList 的父类 AbstractList 的一个字段, 这个字段的含义是 list 结构发生变更的次数, 通常是 add 或 remove 等导致元素数量变更的会触发 modCount++.
下面接着看 itr.hasNext()``var2.next()的实现.
itr.hasNext 和 itr.next 实现
hasNext 很简单
- public boolean hasNext() {
- return cursor != size;
- }
当前 index 不等于 size 则说明还没迭代完, 这里的 size 是外部类 ArrayList 的字段, 表示元素个数.
在看 next 实现:
- 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];
- }
- final void checkForComodification() {
- if (modCount != expectedModCount)
- throw new ConcurrentModificationException();
- }
next 方法第一步 checkForComodification(), 它做了什么? 如果 modCount != expectedModCount 就抛出异常 ConcurrentModificationException.modCount 是什么? 外部类 ArrayList 的元素数量变更次数; expectedModCount 是什么? 初始化内部类 Itr 的时候外部类的元素数量变更次数.
所以, 如果在 foreach 中做了 add 或者 remove 操作会导致程序异常 ConcurrentModificationException. 这里可以走两个例子:
- @Test(expected = ConcurrentModificationException.class)
- public void testListForeachRemoveThrow() {
- List<String> list = new ArrayList<>();
- list.add("1");
- list.add("2");
- list.add("3");
- for (String s : list) {
- list.remove(s);
- }
- }
- @Test(expected = ConcurrentModificationException.class)
- public void testListForeachAddThrow() {
- List<String> list = new ArrayList<>();
- list.add("1");
- list.add("2");
- list.add("3");
- for (String s : list) {
- list.add(s);
- }
- }
单元测试跑过, 都抛了 ConcurrentModificationException.
checkForComodification()之后的代码比较简单这里就不分析了.
倒数第二个元素的特殊
到这里我们来捋一捋大致的流程:
获取到 Itr 对象赋值给 var2
判断 hasNext, 也就是判断 cursor != size, 当前迭代元素下标不等于 list 的个数, 则返回 true 继续迭代; 反之退出循环
next 取出迭代元素
checkForComodification(), 判断
modCount != expectedModCount
, 元素数量变更次数不等于初始化内部类 Itr 的时元素变更次数, 也就是在迭代期间做过修改就抛
- ConcurrentModificationException
- .
如果检查通过 cursor++
下面考虑一种情况: remove 了倒数第二个元素会发生什么? 代码如下:
- @Test
- public void testListForeachRemoveBack2NotThrow() {
- List<String> list = new ArrayList<>();
- list.add("1");
- list.add("2");
- list.add("3");
- for (String s : list) {
- System.out.println(s);
- if ("2".equals(s)) {
- list.remove(s);
- }
- }
- }
猜一下会抛出异常吗? 答案是否定的. 输出为:
1
2
发现少了 3 没有输出. 分析一下
在倒数第二个元素 "2"remove 后, list 的 size-1 变为了 2, 而此时 itr 中的 cur 在 next 方法中取出元素 "2" 后, 做了加 1, 值变为 2 了, 导致下次判断 hasNext 时, cursor==size,hasNext 返回 false, 最终最后一个元素没有被输出.
如何避坑
foreach 中 remove 或 add 有坑,
在 foreach 中做导致元素个数发生变化的操作 (remove, add 等) 时, 会抛出
ConcurrentModificationException
异常
在 foreach 中 remove 倒数第二个元素时, 会导致最后一个元素不被遍历
那么我们如何避免呢? 不能用 foreach 我们就用 fori 嘛, 如下代码:
- @Test
- public void testListForiMiss() {
- List<String> list = new ArrayList<>();
- list.add("1");
- list.add("2");
- list.add("3");
- for (int i = 0; i <list.size(); i++) {
- System.out.println(list.get(i));
- list.remove(i);
- }
- }
很明显上面是一个错误的示范, 输出如下:
1
3
原因很简单, 原来的元素 1 被 remove 后, 后面的向前拷贝, 2 到了原来 1 的位置 (下标 0),3 到了原来 2 的位置(下标 1),size 由 3 变 2,i+1=1, 输出 list.get(1) 就成了 3,2 被漏掉了.
下面说下正确的示范:
方法一, 还是 fori, 位置前挪了减回去就行了, remove 后 i--:
- @Test
- public void testListForiRight() {
- List<String> list = new ArrayList<>();
- list.add("1");
- list.add("2");
- list.add("3");
- for (int i = 0; i <list.size(); i++) {
- System.out.println(list.get(i));
- list.remove(i);
- i--; // 位置前挪了减回去就行了
- }
- }
方法二, 不用 ArrayList 的 remove 方法, 用 Itr 自己定义的 remove 方法, 代码如下:
- @Test
- public void testIteratorRemove() {
- List<String> list = new ArrayList<>();
- list.add("1");
- list.add("2");
- list.add("3");
- Iterator<String> itr = list.iterator();
- while (itr.hasNext()) {
- String s = itr.next();
- System.out.println(s);
- itr.remove();
- }
- }
为什么 itr 自己定义的 remove 就不报错了呢? 看下源码:
- 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();
- }
- }
依然有 checkForComodification()校验, 但是看到后面又重新赋值了, 所以又相等了.
ok, 以上就是全部内容. 介绍了 foreach 中 list remove 的坑, 以及如何避免.
来源: https://www.cnblogs.com/chrischennx/p/9610853.html