【问题标题】:ArrayList.remove gives different result when called as Collection.removeArrayList.remove 在称为 Collection.remove 时会给出不同的结果
【发布时间】:2016-06-11 08:59:50
【问题描述】:

这段代码:

    Collection<String> col = new ArrayList<String>();    
    col.add("a");
    col.add("b");
    col.add("c");
    for(String s: col){       
       if(s.equals("b"))
             col.remove(1);  
       System.out.print(s);  

    } 

打印:abc

同时这个:

    ArrayList<String> col = new ArrayList<String>();    
    col.add("a");
    col.add("b");
    col.add("c");
    for(String s: col){       
       if(s.equals("b"))
             col.remove(1);  
       System.out.print(s);  

    } 

打印:ab

但是它应该打印相同的结果... 有什么问题?

【问题讨论】:

    标签: java collections iteration


    【解决方案1】:

    Collection 只有boolean remove(Object o) 方法,如果找到,则删除传递的对象。

    ArrayList 也有public E remove(int index),可以通过索引删除元素。

    您的第一个 sn-p 调用 boolean remove(Object o),它不会删除任何内容,因为您的 ArrayList 不包含 1。您的第二个 sn-p 调用 public E remove(int index) 并删除索引为 1 的元素(即删除 "b")。

    不同的行为是由于方法重载决议发生在编译时发生的,并且取决于您为其调用方法的变量的编译时类型。当col 的类型为Collection 时,仅考虑Collection 接口的remove 方法(以及该接口继承的方法)进行重载解析。

    如果将 col.remove(1) 替换为 col.remove("b"),则两个 sn-ps 的行为将相同。

    正如 Tamoghna Chowdhury 评论的那样,boolean remove(Object o) 可以接受原始参数 - 在您的情况下为 int - 由于 int 自动装箱到 Integer 实例。对于第二个 sn-p,之所以选择 public E remove(int index) 而不是 boolean remove(Object o) 是因为方法重载解析过程首先尝试找到匹配方法而不进行自动装箱/拆箱转换,因此它只考虑 public E remove(int index)

    【讨论】:

    • 您能否添加有关Collections.remove() 方法由于自动装箱而接受int 的部分?
    • @TamoghnaChowdhury 确定
    • 添加 ArrayList 的另一件事是快速收集,因此如果您要在遍历列表期间从列表中删除任何元素,而不是使用 CopyOnWriteArrayList,因为它是故障安全的
    • 还应该补充一点,在迭代集合的同时修改集合通常是一个坏主意。有些语言甚至不允许这样做。这就是您看不到“c”被打印出来的原因。它仍然存在(仅删除了“b”),但由于已删除,您已提前一步到达集合的“结尾”。
    【解决方案2】:

    要在迭代 Collection 时安全地删除它,您应该使用 Iterator

    ArrayList<String> col = new ArrayList<String>();    
    col.add("a");
    col.add("b");
    col.add("c");
    
    Iterator<String> i = col.iterator();
    while (i.hasNext()) {
       String s = i.next(); // must be called before you can call remove
       if(s.equals("b"))
          i.remove();
       System.out.print(s);
    }
    

    关于,在ArrayList 有效时,从集合中删除对您不起作用的原因如下:

    1. java.util.ArrayList.remove(int index) 方法删除此列表中指定位置的元素。将任何后续元素向左移动(从它们的索引中减去 1)。因此,这个对你有用。

    2. java.util.Collection.remove(Object o) 方法从该集合中删除指定元素的单个实例(如果存在)(这是一个可选操作)。更正式地说,如果此集合包含一个或多个此类元素,则删除元素 e 使得 (o==null ? e==null : o.equals(e))。如果此集合包含指定元素(或等效地,如果此集合因调用而更改),则返回 true

    希望,这会有所帮助。

    【讨论】:

      【解决方案3】:

      两个 sn-ps 的破坏方式不同!

      案例 1(Collection&lt;String&gt; col):

      由于Collection 没有索引,其接口公开的唯一remove 方法是Collection.remove(Object o),它会删除指定的相等对象。执行col.remove(1); 首先调用Integer.valueOf(1) 以获取Integer 对象,然后要求列表删除该对象。由于该列表不包含任何此类Integer 对象,因此不会删除任何内容。在列表中正常进行迭代并打印出abc

      案例 2(ArrayList&lt;String&gt; col):

      col的编译时类型为ArrayList时,调用col.remove(1);改为调用ArrayList.remove(int index)方法移除指定位置的元素,从而移除b

      现在,为什么没有打印出c?为了使用for (X : Y) 语法遍历集合,它在后台调用集合以获取Iterator 对象。对于ArrayList(以及大多数集合)返回的Iterator在迭代期间对列表进行结构修改是不安全的——除非您通过Iterator 本身的方法对其进行修改– 因为Iterator 会变得混乱并且不知道接下来要返回哪个元素。这可能导致元素被多次迭代、元素被跳过或其他错误。这就是这里发生的情况:元素 c 存在于列表中,但从未打印出来,因为您混淆了 Iterator

      Iterator 可以检测到此问题已发生时,它会通过抛出ConcurrentModificationException 来警告您。但是,Iterator 针对问题所做的检查针对速度进行了优化,而不是 100% 正确,并且它并不总是能检测到问题。在您的代码中,如果您将s.equals("b") 更改为s.equals("a")s.equals("c"),它会抛出异常(尽管这可能取决于特定的Java 版本)。来自ArrayList documentation

      这个类的iteratorlistIterator 方法返回的迭代器是fail-fast:如果列表在创建迭代器后的任何时候在结构上被修改,除了通过迭代器自己的removeadd 方法,迭代器会抛出一个ConcurrentModificationException。因此,面对并发修改,迭代器快速而干净地失败,而不是在未来不确定的时间冒任意的、非确定性的行为。

      请注意,无法保证迭代器的快速失败行为,因为一般来说,在存在不同步的并发修改的情况下无法做出任何硬保证。快速失败的迭代器会尽最大努力抛出ConcurrentModificationException


      要在迭代期间删除元素,您必须将循环的for (X : Y) 样式更改为显式Iterator 上的手动循环,使用其remove 方法:

      for (Iterator<String> it = col.iterator(); it.hasNext();) {
          String s = it.next();
          if (s.equals("b"))
              it.remove();
          System.out.print(s);
      }
      

      现在这是完全安全的。它将所有元素只迭代一次(打印abc),而元素b 将被删除。

      如果您愿意,您可以在不使用Iterator 的情况下使用int i 样式的循环来实现相同的效果,前提是您在删除后仔细调整索引:

      for (int i = 0; i < col.size(); i++) {
          String s = col.get(i);
          if (s.equals("b")) {
              col.remove(i);
              i--;
          }
          System.out.print(s);
      }
      

      【讨论】:

      • 是的,这个。另外,col.removeIf(s -&gt; s.equals("b")).
      猜你喜欢
      • 2012-06-09
      • 2014-02-01
      • 2021-08-26
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-11-16
      • 2013-01-06
      相关资源
      最近更新 更多