在阿里巴巴Java开发手册中,有这样一条规定:但是手册并没有给出具体原因,本文将分析这条规定的背景深入思考。1.foreach循环foreach循环(Foreachloop)是计算机编程语言中的一种控制流语句,通常用于循环遍历数组或集合中的元素。Java语言从JDK1.5.0开始就引入了foreach循环。在遍历数组和集合方面,foreach为开发者提供了极大的便利。通常也称为增强的for循环。foreach的语法格式如下:for(元素类型t元素变量x:遍历对象obj){指x的java语句;}下面的例子演示了普通for循环和foreach循环的使用:publicstaticvoidmain(String[]args){//使用ImmutableList初始化一个ListListuserNames=ImmutableList.of("Hollis","hollis","HollisChuang","H");System.out.println("使用for循环遍历List");for(inti=0;iuserNames=newArrayList(){{add("Hollis");add("hollis");add("HollisChuang");add("H");}};for(inti=0;iuserNames=newArrayList(){{add("Hollis");add("hollis");add("HollisChuang");add("H");}};for(StringuserName:userNames){if(userName.equals("Hollis")){userNames.remove(userName);}}System.out.println(userNames);上述代码使用增强的for循环遍历元素,并尝试删除Hollis字符串元素。运行上述代码会抛出如下异常:java.util.ConcurrentModificationException同样,读者可以尝试在增强的for循环中使用add方法添加元素,结果也会抛出异常。出现此异常的原因是触发了Java集合的错误检测机制——fail-fast。3.fail-fast接下来我们来分析一下在增强的for循环中添加/删除元素时抛出java.util.ConcurrentModificationException的原因,即解释什么是fail-fast系统和fail-fast原理等。-fast,即快速失败,是Java集合的一种错误检测机制。当多个线程对集合(非故障安全集合类)进行结构更改时,可能会出现fail-fast机制,此时会抛出ConcurrentModificationException(当方法检测到对象并发修改时,但是,this当不允许这样的修改时抛出异常)。同时需要注意的是,即使不是多线程环境,如果单线程违反规则,也有可能会抛出异常。那么,增强型for循环中的元素删除是如何违规的呢?分析这个问题,我们先将增强的for循环的语法糖脱糖(使用jad反编译编译后的class文件),得到如下代码:publicstaticvoidmain(String[]args){//UsingImmutableList初始化一个ListListuserNames=newArrayList(){{add("Hollis");add("hollis");add("HollisChuang");add("H");}};Iteratoriterator=userNames.iterator();do{if(!iterator.hasNext())break;StringuserName=(String)iterator.next();if(userName.equals("Hollis"))userNames.remove(userName);}while(true);System.out.println(userNames);}然后运行上面的代码,同样会抛出异常。我们来看一下ConcurrentModificationException的完整堆栈:通过异常堆栈我们可以看到,在发生异常的调用链ForEachDemo的第23行,Iterator.next调用了Iterator.checkForComodification方法,在checkForComodification方法。其实经过调试我们可以发现,如果没有执行过remove代码,iterator.next这行从来没有报错过。抛出异常的时机恰好是remove执行完后next方法的调用。我们直接看checkForComodification方法的代码,看看为什么会抛出异常:finalvoidcheckForComodification(){if(modCount!=expectedModCount)thrownewConcurrentModificationException();}代码比较简单。当modCount!=expectedModCount时,将抛出ConcurrentModificationException。那么,我们来看看remove/add操作间是如何导致modCount和expectedModCount不相等的。4.删除/添加有什么作用?首先我们要搞清楚modCount和expectedModCount这两个变量是什么。通过查看源码我们可以发现:modCount是ArrayList中的一个成员变量。它表示集合实际被修改的次数。expectedModCount是ArrayList中的一个内部类——Itr中的一个成员变量。expectedModCount表示此迭代器期望集合被修改的次数。它的值在ArrayList.iterator方法被调用时被初始化。只有通过迭代器操作集合时,该值才会改变。Itr是Iterator的一个实现,使用ArrayList.iterator方法得到的迭代器是Itr类的一个实例。它们之间的关系如下:classArrayList{privateintmodCount;publicvoidadd();publicvoidremove();privateclassItrimplementsIterator{intexpectedModCount=modCount;}publicIteratoriterator(){returnnewItr();}}其实看到这里,大概很多人都能猜到为什么expectedModCount和modCount不想在remove/add操作之后等待。通过翻阅代码,我们还可以发现,remove方法的核心逻辑如下:可以看到,它只是修改了modCount,并没有对expectedModCount进行任何操作。简单总结一下,之所以会抛出ConcurrentModificationException,是因为我们的代码使用了增强的for循环,而在增强的for循环中,集合的遍历是通过iterator进行的,而元素的添加/删除是直接使用了Collection类自身的方法。结果,当迭代器遍历时,会发现在不知不觉中删除/添加了一个元素,并抛出异常提醒用户可能发生了并发修改。5、正确的姿势至此,我们已经介绍了foreach循环体中不能直接对集合进行add/remove操作的原因。但是很多时候,我们有对集合进行过滤的需求,比如删除一些元素,那么应该怎么做呢?有以下几种方法可供参考:1、直接使用普通的for循环进行操作。我们说不能在foreach中执行,但是用普通的for循环还是可以的,因为普通的for循环没有使用Iterator遍历,所以根本没有fail-fast测试。ListuserNames=newArrayList(){{add("Hollis");add("hollis");add("HollisChuang");add("H");}};for(inti=0;i<1;i++){if(userNames.get(i).equals("Hollis")){userNames.remove(i);}}System.out.println(userNames);2、直接使用Iterator进行操作除了直接使用普通的for循环外,我们还可以直接使用Iterator提供的remove方法。ListuserNames=newArrayList(){{add("Hollis");add("hollis");add("HollisChuang");add("H");}};Iteratoriterator=userNames.iterator();while(iterator.hasNext()){if(iterator.next().equals("Hollis")){iterator.remove();}}System.out.println(userNames);如果直接使用Iterator的remove方法,那么可以修改expectedModCount的值。然后将不再抛出异常。实现代码如下:3.使用Java8中提供的过滤器在Java8中对集合进行过滤。有一个针对流的过滤操作,可以对原始Stream进行一定的测试,通过测试的元素是离开以生成新流。ListuserNames=newArrayList(){{add("Hollis");add("hollis");add("HollisChuang");add("H");}};userNames=userNames.stream().filter(userName->!userName.equals("Hollis")).collect(Collectors.toList());System.out.println(userNames);4.直接使用Java中的fail-safe集合类,除了一些常见的集合类,还有一些集合类采用了fail-safe机制。这样的集合容器在遍历时并不是在集合内容上直接访问,而是先复制原始集合内容,在复制的集合上遍历。由于迭代是遍历原始集合的副本,遍历过程中对原始集合所做的修改无法被迭代器检测到,因此不会触发ConcurrentModificationException。ConcurrentLinkedDequeuserNames=newConcurrentLinkedDeque(){{add("Hollis");add("hollis");add("HollisChuang");add("H");}};for(StringuserName:userNames){if(userName.equals("Hollis")){userNames.remove();}}复制内容的好处是可以避免ConcurrentModificationException,但是同样的,迭代器无法访问修改后的内容,即:iterationwhattheiteratortraverses是遍历那一刻得到的集合的副本,遍历过程中迭代器并不知道原集合的修改。java.util.concurrent包下的容器是安全失败的,可以在多线程下并发使用和修改。5.也可以使用增强的for循环。如果我们非常确定在一个集合中,一个要删除的元素只包含一个,比如对一个Set进行操作,那么实际上可以使用增强的for循环,只要删除,立即结束循环体,就可以了不要继续遍历,也就是不要让代码执行到下一个next方法。ListuserNames=newArrayList(){{add("Hollis");add("hollis");add("HollisChuang");add("H");}};for(StringuserName:userNames){if(userName.equals("Hollis")){userNames.remove(userName);break;}}System.out.println(userNames);以上五种方法可以避免触发fail-fast机制,避免抛出异常。如果是并发场景,建议使用并发包中的容器。如果是单线程场景,建议在Java8之前的代码中使用Iterator删除元素。在Java8及之后的版本中,可以考虑使用Stream和filter。6.小结我们使用的增强型for循环其实是Java提供的一个语法糖,它的实现原理是使用Iterator来遍历元素。但是如果在遍历过程中,通过集合类本身的方法而不是Iterator来添加/删除集合。然后,当Iterator进行下一次遍历时,检测到一个集合修改操作还没有被自己执行,那么可能会被其他线程并发执行,此时会抛出异常提醒用户它可能会发生这就是所谓的fail-fast机制。当然,解决这类问题的方法还有很多。比如使用普通的for循环,使用Iterator删除元素,使用Streamfilter,使用fail-safe类等等。好了,以上就是本文的全部内容。主要介绍阿里巴巴Java开发手册中foreach循环体中禁止增删元素的原因和原则。【本文为专栏作家霍利斯原创文章,作者微信公众号Hollis(ID:hollishuang)】点此阅读更多本作者好文