`
nada_forever
  • 浏览: 24858 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
社区版块
存档分类
最新评论

从ArrayList.remove方法想到的

阅读更多

 

引子

前几天一位同学在找bug时候,发现了自己犯下了一个基本错误,程序原型如下

       List<String> source = new ArrayList<String>();

       source.add("one");

       source.add("two");

       source.add("three");

       source.add("four");

       source.add("five");

 

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

           String s = source.get(i);

           if (s.equals("two")||s.equals("three"))

              source.remove(s);

       }

       System.out.println("output result after delete:");

       for(String s : source)

          System.out.println(s);

功能是将列表中值为“two”与“three”的项删除,然后打印出剩余的项,

以上程序的输出为:

--------------------------

    output result after delete:

    one

    three

    four

    five

 

--------------------------

three”没有被删除,原因也是比较明显,就是在遍历过程时,当删除了当前项后,ArrayList会对内部数据进行整理,导致索引与size的变化,造成“漏删”的情况。

那么,如何解决?

一种方法如下:

--------------------------

       for (int i = source.size()-1; i >= 0; i--) {

           String s = source.get(i);

           if (s.equals("two")||s.equals("three"))

              source.remove(s);

   }

--------------------------

以上程序将索引从前往后的遍历方式变更为从后往前,OK,问题解决了,那么,还有更简单的办法吗?这时,这位同学提出一个想法,也是本文要讨论的重点:

能否使用java5for-each语法遍历列表,并删除目标项?

 

好,我们来尝试一下,将程序修改为:

--------------------------

       for(String s : source){

           if (s.equals("two")||s.equals("three"))

              source.remove(s);

   }

--------------------------

看起来还不错,语法更简单了,阅读更容易了,运行一下吧;

结果输出:

 

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 remove.TestRemove.main(TestRemove.java:31)

 

这?为什么?ConcurrentModificationException?并发修改错误!在单线程中,也会有并发的错误?

 

过程

    在解释上面看似令人费解的现象之前,我们先明确如下两点事实:

    1java5内置的for-each语法实际上是Iterable的变体,也就是说,只有实现了Iterable接口的类的对象,才适用for-each语法,因此,我们可以将上面的程序修改为:

       for(Iterator<String> it = source.iterator();it.hasNext();){

           String s = it.next();

           if (s.equals("two")||s.equals("three"))

              source.remove(s);

       }

上述程序本质上与使用for-each语法并无差别。

    2ArrayList并非线程安全,只有在单线程的情形下才可以保证状态一致,当调用者调用iterator()方法对ArrayList进行遍历的过程期间,是不允许进行任何的元素新增与删除的,一旦在遍历期间发现有任何变化(在next方法中检查),则抛出ConcurrentModificationException异常,以便调用者知晓。

    上述约束是这样实现的:

    ArrayList中中维护了一个私有变量modCount,类似于版本的变更版本号;

protected transient int modCount = 0;

在对象构建时,该变量初始值为0,之后,在调用者调用addremove方法时,modCount会进行自增来标识版本的变更;当调用者调用iterator()方法时,ArrayList会构造一个全新的Iterator对象返回给调用者,以便调用者通过操作该IteratorArrayList进行遍历;

    public Iterator<E> iterator() {

        return new Itr();

}

 

Itr是内部类,实现如下:

    private class Itr implements Iterator<E> {

        int cursor = 0;

        int lastRet = -1;

        int expectedModCount = modCount;

 

        public boolean hasNext() {

            return cursor != size();

        }

 

        public E next() {

         checkForComodification();

            try {

              E next = get(cursor);

              lastRet = cursor++;

              return next;

            } catch (IndexOutOfBoundsException e) {

              checkForComodification();

              throw new NoSuchElementException();

            }

        }

 

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

                }

           }

 

        final void checkForComodification() {

            if (modCount != expectedModCount)

              throw new ConcurrentModificationException();

        }

 }

关键是这一句:int expectedModCount = modCount;

在构建Itr时,会把ArrayList当前的modCount赋予Itr,并由Itr内部使用expectedModCount进行保存,并且,在Itr的实例返回给调用者后,expectedModCount的值不会变化,当调用者调用Itrnext方法进行遍历时,会在next方法内部调用checkForComodification方法进行条件检查,checkForComodification方法会比较当前ArrayListmodCountItrexpectedModCount是否相等,如果相等,则可以继续操作,如果不相等,则说明在调用者遍历的过程中,内部数据元素已经被修改过,则抛出ConcurrentModificationException异常。

 

上述两点事实是我们分析的基础,具体到我们的程序,正是因为在调用者遍历Itr的过程中,调用了ArrayListremove方法,导致ArrayList内部的modCount自增,然后ItrexpectedModCount并没有变化,之后调用者再调用next时,导致抛出ConcurrentModificationException异常,这就是之前奇怪现象的原因。

 

接下来,我们的功能需求有一些变化:只删除值为“four”的项。

我们把程序改一改,但仍然使用之前的遍历与删除方式:

       for(Iterator<String> it = source.iterator();it.hasNext();){

           String s = it.next();

           if (s.equals("four"))

              source.remove(s);

    }

       System.out.println("output result after delete:");

       for(String s : source)

       System.out.println(s);

结果会如何?按刚才的分析,抛出ConcurrentModificationException异常,对吧?

可结果是:

    output result after delete:

    one

    two

    three

five

 

输出是正确的!

 

问题出在哪儿?很简单,调用者在调用next方法之前,会先调用hasNext进行检测是否存在下一个元素,如果存在,则继续调用next,如果不存在,说明之前已经遍历了所有的元素,可以结束了;

在看看ItrhasNext的实现:

        public boolean hasNext() {

            return cursor != size();

        }

它会根据当前的遍历游标与ArrayListsize进行比较,如果不相等才认为下一个节点存在,而我们的“four”元素是倒数第二个元素,而刚才之前又调用了ArrayListremove方法,导致size1,正好让cursorsize()相等,因此,调用者就不会继续调用next方法了,也就不会检查modCount,自然不会抛出ConcurrentModificationException异常了,且输出的结果也是正确的;

这是巧合,又是合理的。

 

总结

问题的原因我们清楚了,就该指导实践了。

下面是总结的经验:

如果想在遍历时删除ArrayList中的元素,可以通过下面几种方法:

1、“从后往前”通过索引值进行遍历,并通过当前索引值对元素进行删除;

2、使用显式的IteratorArrayList进行遍历,并通过iterator.remove方法对元素进行删除,而不要使用ArrayListremove方法;例子:

       for(Iterator<String> it = source.iterator();it.hasNext();){

           String s = it.next();

           if (s.equals("two")||s.equals("three"))

              it.remove();

    }

         切忌:不要使用for-eachArrayList进行遍历,并调用ArrayListremove方法对元素进行删除(如果对类似CopyOnWriteArrayList这样的并发集合进行for-each,并调用其自身的remove方法,是没有问题的,原因是该类集合专门对并发环境的使用进行了调整,本文只关注ArrayList)。

 

分享到:
评论

相关推荐

    Java中ArrayList的removeAll方法详解

    ArrayList的removeAll方法是一个常用的集合操作方法,该方法可以从一个ArrayList中删除所有在另外一个集合中的元素。但是,在实际开发过程中,removeAll方法的使用需要 thận重,因为它可能会导致性能问题。 1. ...

    List.removeAll() 方法的性能效率

    在Java编程语言中,`List.removeAll()`方法是一个非常实用的函数,它允许我们从列表中一次性移除所有指定元素。这个方法是集合框架的一部分,它提供了高效的方式来进行元素的删除操作。本文将深入探讨`removeAll()`...

    java ArrayList.remove()的三种错误用法以及六种正确用法详解

    以下是对`java ArrayList.remove()`的三种错误用法以及六种正确用法的详细说明。 **错误用法:** 1. **错误用法1.1 - 在for循环中使用`remove(int index)`** 在for循环中直接删除元素可能导致遍历过程中的索引...

    $ArrayList.jar

    模仿sun公司的ArrayList自己封装了一个容器类,能基本实现全部功能,在数组的一些方法上进行了修改优化

    ArrayList.md

    老猿说说-ArrayList MD文件 1. 所有的操作都是线程安全的,我们在使用时,无需再加锁; 2. 多个线程同时进行put、remove等操作时并不会阻塞,可以同时进行,和HashTable不同,HashTable在操作时,会锁住整个Map; 3. ...

    arraylist .

    arraylist .

    Java Methods-java.util.ArrayList.ppt

    从 Java 5 开始,ArrayList 和其他集合类可以指定元素的类型,例如: ```java ArrayList&lt;String&gt; words = new ArrayList(); List&lt;Integer&gt; nums = new ArrayList(); ``` 七、ArrayList 类的注意事项 ArrayList 类...

    Arabic2ArrayList.rar

    "Arabic2ArrayList.rar"这个压缩包文件显然与阿拉伯语的显示和处理有关,尤其是针对单片机系统。让我们深入探讨一下其中涉及的关键知识点。 1. **字符编码**:阿拉伯语属于右向左(RTL)语言,其字符编码通常遵循...

    java提高篇(二一)-----ArrayList.pdf

    2. 实现了所有可选列表操作:ArrayList 实现了 List 接口,提供了诸如 add、remove、get 等方法。 3. 允许包括 null 元素:ArrayList 允许包括 null 元素在内的所有元素。 4. 提供了操作内部数组的方法:ArrayList ...

    ArrayList测试.

    myArrayList.RemoveAt(0); ``` 在描述中提到的`Delete()`方法实际上并不是ArrayList的内置方法。在C#中,我们通常使用`RemoveAt()`来删除元素。但如果想要模拟类似`Delete()`的行为,可以编写一个自定义方法,如: ...

    JavaScript 实现基础 ArrayList 功能

    为了提供更丰富的功能,还可以实现更多的方法,如清空ArrayList的`clear`方法、检查是否包含特定值的`contains`方法、合并两个ArrayList的`concat`方法等。 ```javascript ArrayList.prototype.clear = function() ...

    java中数组列表ArrayList的使用.doc

    4. Add、AddRange、Remove、RemoveAt、RemoveRange、Insert、InsertRange 方法 这些方法用于添加、删除和插入元素到 ArrayList 中。 5. TrimSize 方法 这个方法用于将 ArrayList 固定到实际元素的大小,当动态数组...

    ArrayList.cpp

    ArrayList.cpp

    ArrayList.java

    ArrayList.java

    arraylist.java

    arraylist.java

    ArrayList常用的方法

    - `Remove` 方法用于从 `ArrayList` 中移除第一个匹配到的指定对象。 - 如果 `ArrayList` 中包含多个相同的对象,则只移除第一次出现的对象。 **示例代码**: ```csharp arrlist.Remove("第二个元素"); // 输出...

    集合之ArrayList.md

    Arraylist

Global site tag (gtag.js) - Google Analytics