java集合中删除元素问题

  1. 执行下面代码

向集合中添加5 个元素,然后删除”bbb”


public static void main(String[] args) {  
          List<String> list=new ArrayList<String>(); 
          list.add("aaa"); 
          list.add("bbb"); 
          list.add("ccc"); 
          list.add("ddd"); 
          list.add("eee"); 
          for (String str : list) { 
                System.out.println(str); 
                if("bbb".equals(str)){ 
                      list.remove(str); 
                } 
          } 
    }



运行这段代码 ,结果如下:



aaa 
  bbb 
  Exception in thread "main" java.util.ConcurrentModificationException 
      at 
  java.util.AbstractList$Itr.checkForComodification(AbstractList.java:372
   ) 
        at java.util.AbstractList$Itr.next(AbstractList.java:343) 
        at com.wq.Test.main(Test.java:15)



  1. 分析
    出现了ConcurrentModificationException 的异常.为什么会出现这个异常?。如果 删除的不是”bbb”,而是”ddd”, (倒数第二个)则不会产生异常。 即:
public static void main(String[] args) { 
    List<String> list=new ArrayList<String>(); 
   list.add("aaa"); 
   list.add("bbb"); 
   list.add("ccc"); 
   list.add("ddd"); 
   list.add("eee"); 
   for (String str : list) { 
          System.out.println(str); 
          if("ddd".equals(str)){ 
                list.remove(str); 
 
          } 
   }  
  }



这段代码能正常运行 ,这又是为何?

  1. Java 中 for each 循环的运行机制

上面代码中的for 循环是JDK 1.5 开始出现的一种循环的写法。它可以用于迭代数组 或者泛型集合。那么它是如何作用在集合上的呢? 查看异常消息 ,我们发现异常消息是从AbstractList$Itr 中的next 方法中抛出 来的。ArrayList 是 AbstractList 类的子类打开 AbstractList 的源码,发现 Itr 实际上是 AbstractList 中的内部类: 定义如下: .



private class Itr implements Iterator<E>



原来for each 循环还是利用了迭代器来遍历集合的 ,这个迭代器是一个内部类。当我们使用集合对象的 iterator 方法获取迭代器对象的时候 ,获取到的迭代器就是这个内部类的实例。



public Iterator<E> iterator() { 
        return new Itr(); 
   }



所以可以在内部类的hasNext 方法上打上一个断点,然后使用debug 进行调试,发现进入了断点 ,这证明for each 循环确实使用的是迭代器来遍历集合的。

  1. 跟踪代码 ,找出抛出异常的原因 for each 循环开始的时候 ,首先调用迭代器的hasNext 方法判断是否继续循环 ,代 码如下:
public boolean hasNext() { 
              return cursor  != size(); 
       }



cursor 是Itr 内部类(迭代器)中定义的变量将其翻译为游标 ,初始值是0 ,相当于 当前的索引号。而 size ()则是获取集合元素的个数.如果索引号与集合元素个数相同了 ,那么返回false ,不同则返回true ,循环继续。 接下来调用Itr 内部类(迭代器)中定义的next 方法获取集合中的元素:



public E next() { 
        checkForComodification(); 
        try  { 
         E next = get(cursor); 
 
         lastRet = cursor++; 
 
         return next; 
        } catch  (IndexOutOfBoundsException e) { 
         checkForComodification(); 
         throw new NoSuchElementException(); 
 
        } 
 
    }



进入next 方法首先是调用checkForcomodification 方法 ,这个方法是在内部类(迭 代器)中定义的 :



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

      }



这个方法的主要用处在于判断集合中的元素个数是否发生变化。如果发生变化,就会抛出ConcurrentModificationException 异常 modCount 在AbstractList 类(迭代器所在的外部类)中定义,用来统计更改的 次数。expectedModCount 在迭代器中定义 ,表示期望的更改的次数 ,迭代器刚开始创 建的时候,它的值与modCount 是一致的 ,迭代器开始工作以后,只要集合中的元素进行了增加后者删除,也就是元素的个数发生变化,那么modCount 就会发生变化,这样就与 期望值expectedModCount不一致,此时就会抛出 ConcurrentModificationException 异常。 上面的程序中,连续添加了 5 个元素 ,也就是调用了5 次add 方法 ,我们可以看看 这个方法的源码 (ArrayList 类的add 方法) :



public boolean add(E e) { 
        ensureCapacity(size + 1);  // Increments modCount!! 
        elementData [size++] = e; 

        return true; 
     
      }



ensureCapacity 方法:



public void ensureCapacity(int minCapacity) { 
     
         modCount++; 
         int oldCapacity = elementData.length; 
         if  (minCapacity > oldCapacity) { 
     
             Object oldData[] = elementData; 
     
             int newCapacity = (oldCapacity * 3)/2 + 1; 
             if  (newCapacity < minCapacity) 
               newCapacity = minCapacity; 
                // minCapacity is usually close to size, so this is a win: 
     
                elementData = Arrays.copyOf(elementData, newCapacity); 
     
          } 
        }



在ensureCapacity 方法中,将 modCount 增加了 ,所以在for each 循环开始的 时候,modCount 中的值是5, expectedModCount 的值也是5. 此时checkForcomodification 没有抛出异常,那么 next 方法就会继续向下执行, 取出当前的对象,然后将当前指针(索引号)cursor 的值增加1。 当取出”bbbb”元素之后,由于满足条件 ,所以要执行删除操作 ,即调用 remove 方法 ,remove 的源码如下:



public boolean remove(Object o) {
        if (o == null) {
            for (int index = 0; index < size; index++)
            if (elementData[index] == null) {
            fastRemove(index);
            return true;
           }
        } else {
            for (int index = 0; index < size; index++)
            if (o.equals(elementData[index])) {
            fastRemove(index);
            return true;
            }
        }
        return false;
    }



在要删除的元素不为空的情况下 ,遍历elementData 数组 (这个数组就是存储数据的数据源),找出要删除的元素的索引号 ,然后调用fastRemove 方法,源码如下:



private void fastRemove(int index) { 
                   modCount++; 
                   int numMoved = size - index - 1; 
                   if (numMoved > 0) 
                     System.arraycopy (elementData, index+1, elementData, index,   numMoved); 
                   elementData[--size] = null; // Let gc do its work 
           }



可以看到modCount 的值发生了改变,此时变成了6。当前要删除的元素被正确删除掉了。 当”bbb”被删除之后,继续下一次循环。当调用迭代器的 next 方法获取数据的时 候 ,首先调用 checkForComodification 方法进行检查 ,检查的时候,modCount的值为6 ,但是expectModCount 的值依然为5 ,所以抛出异常。

  1. 为什么删除倒数第二个不抛出异常
    如果删除倒数第二个 ,则不会抛出异常,这是为什么? 当正确的删掉了倒数第二个元素之后,马上进行下一次循环 ,此时调用了hasNext 方 法 ,这个方法中是在比较cursor 与元素个数 ,此时的元素个数是4 个 ,因为删除了一个 元素 ,而cursor 的值也是 4 ,所以 hasNext 返回 false ,循环结束。在调用 checkForComodification方法之前,循环就结束了 ,所以没有抛出异常。
  2. 小结
    通过上面的分析,我们找出了抛异常的原因。即使用for each 方法进行迭代的时候, 使用的是迭代器,这个迭代器是一个内部类。取得迭代器的时候 ,迭代器中保存着集合元 素的个数。迭代器每次取出一个元素之前需要判断集合中的元素个数是否发生了改变,如 果改变,则抛出异常。
  3. 执行下面代码
public static void main(String[] args) { 
        List<String> list = new ArrayList<String>(30); 
        list.add("aaa"); 
        list.add("bbb"); 
        list.add("ccc"); 
        list.add("ddd"); 
        list.add("eee"); 

        for(int i=0;i<list.size();i++){ 
              String str=list.get(i); 
         
              if("bbb".equals(str)){ 
                   list.remove(str);      
              } 
        } 
              System.out.println(list); //查看集合中的元素 

  }



执行的结果是:

[aaa, ccc, ddd, eee] 元素被正确删除了。这里没有涉及到迭代器 ,也就没有对集合是否 发生改变进行判断 ,所以不会抛出异常。

  1. 执行下面代码
public static void main(String[] args) { 
        List<String> list = new ArrayList<String>(30); 
        list.add("aaa"); 
        list.add("bbb"); 
        list.add("ccc"); 
        list.add("ddd"); 
        list.add("eee"); 

        for(Iterator<String> it=list.iterator();it.hasNext();){ 
              String str=it.next(); 
              if("bbb".equals(str)){ 
                 list.remove(str); //抛出异常 
              } 
        } 
        //查看集合中的元素 
        System.out.println(list); 
 

 }



虽然这里没有用到 for each 循环 ,但是用到了迭代器,所以上面的代码与使用 for each 代码实际上是一样的 ,按照上面的分析,它一定会抛出异常。

  1. 执行下面的代码
public static void main(String[] args) { 
        List<String> list = new ArrayList<String>(30); 
        list.add("aaa"); 
        list.add("bbb"); 
        list.add("ccc"); 
        list.add("ddd"); 
        list.add("eee"); 

        for(Iterator<String> it=list.iterator();it.hasNext();){ 
               String str=it.next(); 
         
               if("bbb".equals(str)){ 
                     it.remove();//使用迭代器删除,不会抛出异常 
               } 
        }



使用迭代器进行删除,一切正常。查看迭代器的源代码(AbstractList 类中的Itr 内部 类)中的remove 方法:



public void remove() { 
        if (lastRet == -1) 
            throw new IllegalStateException(); 
            checkForComodification(); 
        try { 
            AbstractList.this.remove(lastRet); 
            if (lastRet < cursor) 
            cursor--; 
            lastRet = -1; 
            expectedModCount = modCount; 
        } catch (IndexOutOfBoundsException e) { 
        throw new ConcurrentModificationException(); 
        } 
    }



lastRet 在Itr 内部类中定义,表示最后操作的元素的索引号,每次迭代器的next 方法执行之后,lastRet 的值都会加1 ,但是一旦迭代器执行了 remove 方法之后,lastRet 被重新赋值为-1 ,expectedModCount 与 modCount 被更新成相等。所以迭代器在执行next 方法的时候,调用checkForComodfification()进行检查的时候,不会抛出异常。

  1. 总结
    对集合进行删除的时候,是否抛出 ConcurrentModificationException 异常 ,与使用哪个 remove 方法有关系。如果使用的是集合对象的 remove 方法,modCount 的值就会改变 ,此时通过迭代器的next 方法获取元素 ,就会进行检查,此时就会抛出异常。如果使用的是迭代器的 remove 方法,则不会改变 modCount 的值 ,通过迭代器next方法元素的时候,不会抛出异常。 所以对于在对集合进行迭代的时候删除其中的元素,最好使用迭代器的remove 方法 进行删除