java 为什么遍历的时候不能删除元素详解编程语言

阿里巴巴java开发手册的建议

在看阿里巴巴java开发手册时,有一条建议是这样的。

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

反例:

 List<String> a = new ArrayList<String>();  
 a.add("1"); 
 a.add("2"); 
 for (String temp : a) {  
	if("1".equals(temp)){ 
		 a.remove(temp);  
	 }  
 }  

说明:这个例子的执行结果会出乎大家的意料,那么试一下把“1”换成“2”,会是同样的结果吗?
正例:

Iterator<String> it= a.iterator();  
while(it.hasNext()){  
	String temp = it.next();  
	if(删除元素的条件){  
		it.remove();  
	}  
} 

以上就是手册的建议内容。

我的测试

试验结果表明,为“1”时没有任何错误,“2”时,出现ConcurrentModificationException,也就是并发修改异常。

异常信息如下

Exception in thread "main" java.util.ConcurrentModificationException 
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:859) 
	at java.util.ArrayList$Itr.next(ArrayList.java:831) 
	at com.jun.javase.TestIterator.main(TestIterator.java:16) 

为了看清foreach的真面目,这里将class文件反编译,就明白运行过程了

List a = new ArrayList(); 
a.add("1"); 
a.add("2"); 
Iterator i$ = a.iterator(); 
do 
{ 
    if(!i$.hasNext()) 
        break; 
    String temp = (String)i$.next(); 
    if("1".equals(temp)) 
        a.remove(temp); 
} while(true); 

foreach遍历集合,其实是走的Iterator,首先判断hasNext(),如果没有了则终止循环,否则next()获取元素时,next()时,都要check一下集合元素个数是否变化了,如果变化了,则抛出异常。

查看源代码,Itr是ArrayList的内部类,实现了Iterator接口

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; 
 
 public boolean hasNext() { 
            return cursor != size;//游标不等于元素个数就是还有下一个 
 } 
 
public E next() { 
     checkForComodification();//check是否并发修改 
      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(); 
  } 
} 

modCount是集合添加元素、删除元素的次数,expectedModCount是预期的修改次数。因为我们增加了2个元素,所以,modCount是2,预期修改的次数expectedModCount也是2

现在解释一下,为什么“1”时没有出错,2是却出错了。
“1”时,第一次循环后,光标变为1,第一个元素已经移除,所以元素个数是1;进入第二次循环,判断hasNext()由于光标=元素个数,所以,终止循环,所以,就结束了。

“2”时,第二次循环后,光标变为2,此时元素个数是1,光标!=元素个数,所以进入next()方法,next()中调用checkForComodification,modCount是3,expectedModCount是2,两者不相等,所以抛出异常。

遍历集合删除元素的正确方式

迭代器方式移除

那么如果我们既想遍历元素又想增加/删除元素怎么办?
第一个方法,可以使用迭代器的remove方法,而不是集合的remove方法。这是因为,迭代器的remove方法会修改expectedModCount,从而使modCount与之相等

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(); 
            } 
        } 

迭代器操作元素样例,这种不会出现并发修改异常。

Iterator it = list.iterator(); 
while(it.hasNext()){ 
  it.next(); 
  it.remove(); 
} 

注意,next()方法,不只返回元素,也改变了光标位置。所以如果执行两次next()就会跳过两个元素。

for(int i=0; i<list.size; i++)正序方式

有同学会问,如果使用

for(int i=0; i<list.size(); i++){ 
 
} 

会如何呢?使用这种方法,如果删除元素,那么会跳过某些元素。可以重置index的方式来修正。比如

for(int i=0; i<list.size(); i++){ 
      if(flag){ 
	     //删除元素 
	     //修正index,否则不会出现异常,但是会略过某些元素 
	     i=i-1;	 
	  } 
   
} 

关于这一点,这里解释一下。比如,咱们要遍历一个集合,如果等于1就移除掉,否则全部加1,代码如下

List<String> a = new ArrayList<String>(); 
        a.add("1"); 
        a.add("2"); 
        a.add("3"); 
 
        for (int i = 0; i < a.size(); i++) { 
 
            String e = a.get(i); 
 
            if("1".equals(e)){ 
                a.remove(i); 
                // i=i-1; 修正index 
            }else{ 
                a.set(i,e+"1"); 
            } 
 
        } 
        System.out.println(a); 

运行结果

[2, 31] 

结果出人意料吧?为什么2没有加1?!简单来说,移除元素后,后边的元素会往前移动,将空白填充上
第一次循环,i=0,移除了第一个元素后,i变为1,但是,同时,由于1的空出,2、3都往前移动一个位置,所以,索引位置不再是1、2,而是0、1
第二次循环,i=1,3的索引是1,所以只将3加1,而2已经过去了。

for(int i=list.size-1; i>=0; i – -)倒序方式

由正序方式的缺点为出发点思考,如果倒序的遍历,中间即使有删除也不会漏掉元素。这里就不再举例子了。

List<String> a = new ArrayList<String>(); 
a.add("1"); 
a.add("2"); 
a.add("3"); 
 
 for (int i = a.size()-1; i >=0; i--) { 
 
     String e = a.get(i); 
 
     if("3".equals(e)){ 
         a.remove(i); 
     }else{ 
         a.set(i,e+"1"); 
     } 
 
 } 
 System.out.println(a); 

参考

java核心技术(卷1)

9年全栈开发经验,请关注个人公众号

在这里插入图片描述

原创文章,作者:ItWorker,如若转载,请注明出处:https://blog.ytso.com/20257.html

(0)
上一篇 2021年7月19日
下一篇 2021年7月19日

相关推荐

发表回复

登录后才能评论