首先看下边一个例子,展示了正确的做法和错误的错发:

  这是为什么呢,具体原因下面进行详细说明:

1、foreach循环(Foreach loop)是计算机编程语言中的一种控制流程语句,通常用来循环遍历数组或集合中的元素。Java语言从JDK 1.5.0开始引入foreach循环。在遍历数组、集合方面,foreach为开发人员提供了极大的方便。通常也被称之为增强for循环。其实,增强for循环也是Java给我们提供的一个语法糖,如果将以上代码编译后的class文件进行反编译(使用jad工具)的话,可以得到以下代码:

 Iterator iterator = userNames.iterator();
do
{
if(!iterator.hasNext())
break;
String userName = (String)iterator.next();
if(userName.equals("Hollis"))
userNames.remove(userName);
} while(true);
System.out.println(userNames);

  可以发现,原本的增强for循环,其实是依赖了while循环和Iterator实现的。

  那么,我们再看下,如果使用增强for循环的话会发生什么:

 List<String> userNames = new ArrayList<String>() {{
add("Hollis");
add("hollis");
add("HollisChuang");
add("H");
}}; for (String userName : userNames) {
if (userName.equals("Hollis")) {
userNames.remove(userName);
}
} System.out.println(userNames);

  以上代码,使用增强for循环遍历元素,并尝试删除其中的Hollis字符串元素。运行以上代码,会抛出以下异常:

 java.util.ConcurrentModificationException

  之所以会出现这个异常,是因为触发了一个Java集合的错误检测机制——fail-fast 。

2、接下来,我们就来分析下在增强for循环中add/remove元素的时候会抛出java.util.ConcurrentModificationException的原因,即解释下到底什么是fail-fast进制,fail-fast的原理等。

  fail-fast,即快速失败,它是Java集合的一种错误检测机制。当多个线程对集合(非fail-safe的集合类)进行结构上的改变的操作时,有可能会产生fail-fast机制,这个时候就会抛出ConcurrentModificationException(当方法检测到对象的并发修改,但不允许这种修改时就抛出该异常)。

  同时需要注意的是,即使不是多线程环境,如果单线程违反了规则,同样也有可能会抛出改异常。

  那么,在增强for循环进行元素删除,是如何违反了规则的呢?

  (1)Iterator.next 调用了 Iterator.checkForComodification方法 ,而异常就是checkForComodification方法中抛出的。

 final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}

  (2)modCount是ArrayList中的一个成员变量。它表示该集合实际被修改的次数,remove方法它只修改了modCount,并没有对expectedModCount做任何操作,所以导致抛出java.util.ConcurrentModificationException异常

  简单总结一下,之所以会抛出ConcurrentModificationException异常,是因为我们的代码中使用了增强for循环,而在增强for循环中,集合遍历是通过iterator进行的,但是元素的add/remove却是直接使用的集合类自己的方法。这就导致iterator在遍历的时候,会发现有一个元素在自己不知不觉的情况下就被删除/添加了,就会抛出一个异常,用来提示用户,可能发生了并发修改。

3、正确姿势

  1、直接使用普通for循环进行操作

    我们说不能在foreach中进行,但是使用普通的for循环还是可以的,因为普通for循环并没有用到Iterator的遍历,所以压根就没有进行fail-fast的检验  

    List<String> userNames = new ArrayList<String>() {{
add("Hollis");
add("hollis");
add("HollisChuang");
add("H");
}}; for (int i = 0; i < 1; i++) {
if (userNames.get(i).equals("Hollis")) {
userNames.remove(i);
}
}
System.out.println(userNames);

  2、直接使用Iterator进行操作

    除了直接使用普通for循环以外,我们还可以直接使用Iterator提供的remove方法,如果直接使用Iterator提供的remove方法,那么就可以修改到expectedModCount的值。那么就不会再抛出异常了。其实现代码如下:

     List<String> userNames = new ArrayList<String>() {{
add("Hollis");
add("hollis");
add("HollisChuang");
add("H");
}}; Iterator iterator = userNames.iterator(); while (iterator.hasNext()) {
if (iterator.next().equals("Hollis")) {
iterator.remove();
}
}
System.out.println(userNames);

  3、使用Java 8中提供的filter过滤

    Java 8中可以把集合转换成流,对于流有一种filter操作, 可以对原始 Stream 进行某项测试,通过测试的元素被留下来生成一个新 Stream。

     List<String> userNames = new ArrayList<String>() {{
add("Hollis");
add("hollis");
add("HollisChuang");
add("H");
}}; userNames = userNames.stream().filter(userName -> !userName.equals("Hollis")).collect(Collectors.toList());
System.out.println(userNames);

  4、直接使用fail-safe的集合类

    在Java中,除了一些普通的集合类以外,还有一些采用了fail-safe机制的集合类。这样的集合容器在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。

    由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发ConcurrentModificationException。 

 ConcurrentLinkedDeque<String> userNames = new ConcurrentLinkedDeque<String>() {{
add("Hollis");
add("hollis");
add("HollisChuang");
add("H");
}}; for (String userName : userNames) {
if (userName.equals("Hollis")) {
userNames.remove();
}
}    

    基于拷贝内容的优点是避免了ConcurrentModificationException,但同样地,迭代器并不能访问到修改后的内容,即:迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的。

    java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用,并发修改。

  5、使用增强for循环其实也可以

    如果,我们非常确定在一个集合中,某个即将删除的元素只包含一个的话, 比如对Set进行操作,那么其实也是可以使用增强for循环的,只要在删除之后,立刻结束循环体,不要再继续进行遍历就可以了,也就是说不让代码执行到下一次的next方法

     List<String> userNames = new ArrayList<String>() {{
add("Hollis");
add("hollis");
add("HollisChuang");
add("H");
}}; for (String userName : userNames) {
if (userName.equals("Hollis")) {
userNames.remove(userName);
break;
}
}
System.out.println(userNames);

    以上这五种方式都可以避免触发fail-fast机制,避免抛出异常。如果是并发场景,建议使用concurrent包中的容器,如果是单线程场景,Java8之前的代码中,建议使用Iterator进行元素删除,Java8及更新的版本中,可以考虑使用Stream及filter。

原文链接:https://mp.weixin.qq.com/s/zI0AnGYhkojNl4qVJZaCBQ

为什么禁止在 foreach 循环里进行元素的 remove/add 操作的更多相关文章

  1. 为什么阿里巴巴禁止在 foreach 循环里进行元素的 remove/add 操作--java.util.ConcurrentModificationException

    摘要 foreach循环(Foreach loop)是计算机编程语言中的一种控制流程语句,通常用来循环遍历数组或集合中的元素. 在阿里巴巴Java开发手册中,有这样一条规定: 但是手册中并没有给出具体 ...

  2. 不要在 foreach 循环里进行元素的 remove/add 操作。remove 元素请使用 Iterator 方式,如果并发操作,需要对 Iterator 对象加锁

    不要在 foreach 循环里进行元素的 remove/add 操作.remove 元素请使用 Iterator 方式,如果并发操作,需要对 Iterator 对象加锁. 正例: Iterator&l ...

  3. 为什么阿里巴巴Java开发手册中强制要求不要在foreach循环里进行元素的remove和add操作?

    在阅读<阿里巴巴Java开发手册>时,发现有一条关于在 foreach 循环里进行元素的 remove/add 操作的规约,具体内容如下: 错误演示 我们首先在 IDEA 中编写一个在 f ...

  4. foreach循环中为什么不要进行remove/add操作

    先来看一段代码,摘自阿里巴巴的java开发手册 List<String> a = new ArrayList<String>(); a.add("1"); ...

  5. foreach循环里不能remove/add元素的原理

    foreach循环 ​    foreach循环(Foreach loop)是计算机编程语言中的一种控制流程语句,通常用来循环遍历数组或集合中的元素.Java语言从JDK 1.5.0开始引入forea ...

  6. 有关集合的foreach循环里的add/remove

    转自:Hollis(微信号:hollischuang) 在阿里巴巴Java开发手册中,有这样一条规定: 但是手册中并没有给出具体原因,本文就来深入分析一下该规定背后的思考. 1 .foreach循环 ...

  7. IT兄弟连 Java语法教程 数组 使用foreach循环遍历数组元素

    从JDK5之后,Java提供了一种更简单的循环:foreach循环,也叫作增强for循环,这种循环遍历数组和集合更加简洁.使用foreach循环遍历数组和集合元素时,无需获得数组或集合的长度,无需根据 ...

  8. JavaScript之数组循环 forEach 循环输出数组元素

    var arrayAll = []; arrayAll.push(1); arrayAll.push(2); arrayAll[arrayAll.length] = 3; arrayAll[array ...

  9. JSTL中c:forEach循环里的值的substr操作及对String操作的常用API

    <c:forEach items="${dataList}" var="item" varStatus="itemStatus"> ...

随机推荐

  1. 稀疏矩阵 part 5

    ▶ 目前为止能跑的所有代码及其结果(2019年2月24日),之后添加:DIA 乘法 GPU 版:其他维度的乘法(矩阵乘矩阵):其他稀疏矩阵格式之间的相互转化 #include <stdio.h& ...

  2. hml页面转化成图片

    <!DOCTYPE html><html><head><meta charset="utf-8"><meta name=&qu ...

  3. nginx内容

    nginx工作在7层:web server(静态内容 static contents)web reverse proxy(反向代理http,https,mail),cache(带缓存功能) proxy ...

  4. [JavaScript,Java,C#,C++,Ruby,Perl,PHP,Python][转]流式接口(Fluent interface)

    原文:https://en.m.wikipedia.org/wiki/Fluent_interface(英文,完整) 转载:https://zh.wikipedia.org/wiki/流式接口(中文, ...

  5. PHP:自己写的mysql操作类

    a{ font-weight: bold; display: block; text-align: center; color: #5887bf; font-size: 22px; } .conten ...

  6. Java线程池应用及原理分析(JDK1.8)

    目录 一.线程池优点 二.线程池创建 三.任务处理流程 四.任务缓存队列及排队策略 五.任务拒绝策略 六.线程池关闭 七.线程池实现原理 八.静态方法创建线程池 九.如何确定线程池大小 一.线程池优点 ...

  7. Tensorflow卷积神经网络[转]

    Tensorflow卷积神经网络 卷积神经网络(Convolutional Neural Network, CNN)是一种前馈神经网络, 在计算机视觉等领域被广泛应用. 本文将简单介绍其原理并分析Te ...

  8. Java 字符串拼接四种方式的性能比较分析

    一.简单介绍 编写代码过程中,使用"+"和"contact"比较普遍,但是它们都不能满足大数据量的处理,一般情况下有一下四种方法处理字符串拼接,如下: 1. 加 ...

  9. (ZT)算法杂货铺——k均值聚类(K-means)

    https://www.cnblogs.com/leoo2sk/category/273456.html 4.1.摘要 在前面的文章中,介绍了三种常见的分类算法.分类作为一种监督学习方法,要求必须事先 ...

  10. 【18/12/31】hashcat源码粗读 --- sha256部分

    还没有详细研究过sha256算法的详细原理,主要是移植cf10算法时,hashcat在cf10_parse_hash时并不是直接调用sha256_update和sha256_final, 而是为了pr ...