原文链接:http://www.cnblogs.com/chrischennx/p/9610853.html

都说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()之后的代码比较简单这里就不分析了。

倒数第二个元素的特殊

到这里我们来捋一捋大致的流程:

  1. 获取到Itr对象赋值给var2
  2. 判断hasNext,也就是判断cursor != size,当前迭代元素下标不等于list的个数,则返回true继续迭代;反之退出循环
  3. next取出迭代元素
    1. checkForComodification(),判断modCount != expectedModCount,元素数量变更次数不等于初始化内部类Itr的时元素变更次数,也就是在迭代期间做过修改就抛ConcurrentModificationException
    2. 如果检查通过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()校验,但是看到后面又重新赋值了,所以又相等了。

Java foreach remove问题分析的更多相关文章

  1. java集合源码分析(三):ArrayList

    概述 在前文:java集合源码分析(二):List与AbstractList 和 java集合源码分析(一):Collection 与 AbstractCollection 中,我们大致了解了从 Co ...

  2. java集合源码分析(六):HashMap

    概述 HashMap 是 Map 接口下一个线程不安全的,基于哈希表的实现类.由于他解决哈希冲突的方式是分离链表法,也就是拉链法,因此他的数据结构是数组+链表,在 JDK8 以后,当哈希冲突严重时,H ...

  3. Java foreach

    foreach循环也叫增强型的for循环,或者叫foreach循环. foreach循环是JDK5.0的新特性(其他新特性比如泛型.自动装箱等). import java.util.Arrays; p ...

  4. Java Reference 源码分析

    @(Java)[Reference] Java Reference 源码分析 Reference对象封装了其它对象的引用,可以和普通的对象一样操作,在一定的限制条件下,支持和垃圾收集器的交互.即可以使 ...

  5. Java 线程池原理分析

    1.简介 线程池可以简单看做是一组线程的集合,通过使用线程池,我们可以方便的复用线程,避免了频繁创建和销毁线程所带来的开销.在应用上,线程池可应用在后端相关服务中.比如 Web 服务器,数据库服务器等 ...

  6. Java for-each循环解惑

    Java for-each循环解惑 2014/04/24 | 分类: 技术之外 | 0 条评论 | 标签: JAVA 分享到:21 本文由 ImportNew - liqing 翻译自 javarev ...

  7. 三个实例演示 Java Thread Dump 日志分析

    原文地址: http://www.cnblogs.com/zhengyun_ustc/archive/2013/01/06/dumpanalysis.html jstack Dump 日志文件中的线程 ...

  8. Java NIO原理 图文分析及代码实现

    Java NIO原理图文分析及代码实现 前言:  最近在分析hadoop的RPC(Remote Procedure Call Protocol ,远程过程调用协议,它是一种通过网络从远程计算机程序上请 ...

  9. 三个实例演示 Java Thread Dump 日志分析(转)

    原文链接:http://www.cnblogs.com/zhengyun_ustc/archive/2013/01/06/dumpanalysis.html 转来当笔记^_^ jstack Dump ...

随机推荐

  1. Java-API-Package:java.io

    ylbtech-Java-API-Package:java.io 1.返回顶部 1. Package java.io Provides for system input and output thro ...

  2. PostgreSQL recovery.conf恢复配置

    PostgreSQL recovery.conf恢复配置 这一章描述recovery.conf 文件中可用的设置.它们只应用于恢复期.对于你希望执行的任意后续恢复, 它们必须被重置.一旦恢复已经开始, ...

  3. maven学习6 Eclipse下Tomcat常用设置

    Eclipse下Tomcat常用设置 1,Eclipse建立Tomcat服务 1.1 新建Server 首先这里是指,jee版的Eclipse.Eclipse是没有像MyEclipse那样集成Tomc ...

  4. 设置windows10 背景颜色

    [Win + R ] regedit 打开注册表 HKEY_CURRENT_USER\Control Panel\Colors 1.[InfoWindow] 默认为(白色):255 255 255, ...

  5. c++ 插入排序算法

    第一.算法描述       直插排序很容易理解,在我们打扑克牌的时候,每一次摸完牌,都会按数字大小或者花色,插入到合适的位置,直到摸完最后一张牌,我们手中的牌已经按大小顺序排列好了.这整个过程就是一个 ...

  6. phonegap制作windows phone包

    下载SDK win7及以下版本下载SDK http://www.microsoft.com/zh-cn/download/confirmation.aspx?id=27570 WIN8下载SDK ht ...

  7. Debian 7开启ssh、telnet

    SSH 1. 安装ssh服务 apt-get install openssh-server 2. 开启ssh /etc/init.d/ssh  start Telnet 1. 安装telnet apt ...

  8. C# WinForm 关闭登陆窗体后进程还再内存怎么办?

    问题:我们通常再制作WinForm应用程序的时候,运行程序的第一个窗口一般是登陆窗口.代码如下: 那么这种方式有一个弊端,这种启动方式,其实就是把登陆窗口设置为主窗体.因此,再登陆后,我们通常是调用H ...

  9. Eclipse中如何开启断言(Assert),方法有二

    Eclipse中如何开启断言(Assert),方法有二:1.Run -> Run Configurations -> Arguments页签 -> VM arguments文本框中加 ...

  10. C++面向对象类的实例题目十

    题目描述: 编写一个程序,其中有一个汽车类vehicle,它具有一个需要传递参数的构造函数,类中的数据成员:车轮个数wheels和车重weight放在保护段中:小车类car是它的私有派生类,其中包含载 ...