Java中前缀循环中的调用删除

Calling remove in foreach loop in Java

本问题已经有最佳答案,请猛点这里访问。

在爪哇,当使用Frach循环迭代集合时,调用Read对集合是否合法?例如:

1
2
3
4
5
List<String> names = ....
for (String name : names) {
   // Do something
   names.remove(name).
}

作为附录,删除尚未重复的项是否合法?例如,

1
2
3
4
5
6
//Assume that the names list as duplicate entries
List<String> names = ....
for (String name : names) {
    // Do something
    while (names.remove(name));
}


为了在对集合进行迭代时安全地从集合中移除,应该使用迭代器。

例如:

1
2
3
4
5
6
7
List<String> names = ....
Iterator<String> i = names.iterator();
while (i.hasNext()) {
   String s = i.next(); // must be called before you can call i.remove()
   // Do something
   i.remove();
}

来自Java文档:

The iterators returned by this class's iterator and listIterator
methods are fail-fast: if the list is structurally modified at any
time after the iterator is created, in any way except through the
iterator's own remove or add methods, the iterator will throw a
ConcurrentModificationException. Thus, in the face of concurrent
modification, the iterator fails quickly and cleanly, rather than
risking arbitrary, non-deterministic behavior at an undetermined time
in the future.

对于许多新手来说,可能不清楚的是,使用for/foreach构造对列表进行迭代会隐式地创建一个迭代器,这个迭代器必须是不可访问的。这个信息可以在这里找到


你不想那样做。根据集合,它可能导致未定义的行为。您想直接使用迭代器。尽管for-each结构是语法上的糖分,并且确实使用了迭代器,但它隐藏在代码中,因此您无法访问它来调用Iterator.remove

The behavior of an iterator is
unspecified if the underlying
collection is modified while the
iteration is in progress in any way
other than by calling this method.

而是编写代码:

1
2
3
4
5
6
7
8
List<String> names = ....
Iterator<String> it = names.iterator();
while (it.hasNext()) {

    String name = it.next();
    // Do something
    it.remove();
}

注意,代码调用Iterator.remove,而不是List.remove

附录:

即使要删除尚未迭代的元素,也不希望修改集合,然后使用Iterator。它可能会以一种令人惊讶的方式修改集合,并影响Iterator上的未来操作。


"增强for循环"的Java设计是不将迭代器暴露为代码,但是安全移除项的唯一方法是访问迭代器。因此,在这种情况下,你必须老一套:

1
2
3
4
5
 for(Iterator<String> i = names.iterator(); i.hasNext();) {
       String name = i.next();
       //Do Something
       i.remove();
 }

如果在真正的代码中,增强的for循环确实值得,那么可以将这些项添加到临时集合中,并在循环之后调用列表中的removeall。

编辑(重新补遗):不,在迭代过程中以任何方式在iterator.remove()方法之外更改列表都会导致问题。解决这一问题的唯一方法是使用CopyOnWriteArrayList,但这实际上是针对并发问题的。

移除重复项的最便宜方法(就代码行而言)是将该列表转储到Linkedhashset中(如果需要,还可以将其返回到列表中)。这将在删除重复项时保留插入顺序。


1
2
3
4
for (String name : new ArrayList<String>(names)) {
    // Do something
    names.remove(nameToRemove);
}

克隆列表names,并在从原始列表中删除时迭代克隆。比最上面的答案要干净一点。


我不知道迭代器,但是我今天要做的是从循环内的列表中删除元素:

1
2
3
4
5
List<String> names = ....
for (i=names.size()-1;i>=0;i--) {    
    // Do something    
    names.remove(i);
}

这总是有效的,并且可以在不支持迭代器的其他语言或结构中使用。


是的,您可以使用for each循环,要做到这一点,您必须维护一个单独的列表来保存删除项,然后使用removeAll()方法从名称列表中删除该列表,

1
2
3
4
5
6
7
8
9
10
11
12
List<String> names = ....

// introduce a separate list to hold removing items
List<String> toRemove= new ArrayList<String>();

for (String name : names) {
   // Do something: perform conditional checks
   toRemove.add(name);
}    
names.removeAll(toRemove);

// now names list holds expected values


那些说除了通过迭代器不能安全地从集合中移除某个项的说法是不太正确的,您可以使用并发集合(如ConcurrentHashMap)中的一个来安全地移除该项。


确保这不是代码气味。是否可以反转逻辑,使其"包含"而不是"独占"?

1
2
3
4
5
6
7
8
List<String> names = ....
List<String> reducedNames = ....
for (String name : names) {
   // Do something
   if (conditionToIncludeMet)
       reducedNames.add(name);
}
return reducedNames;

导致我进入这个页面的情况涉及到旧的代码,这些代码使用不雅的行为从列表中删除元素,循环遍历列表。我想重构它以使用foreach样式。

它循环遍历整个元素列表,以验证用户有权访问哪些元素,并从列表中删除没有权限的元素。

1
2
3
4
5
List<Service> services = ...
for (int i=0; i<services.size(); i++) {
    if (!isServicePermitted(user, services.get(i)))
         services.remove(i);
}

要反转此操作而不使用删除操作:

1
2
3
4
5
6
7
List<Service> services = ...
List<Service> permittedServices = ...
for (Service service:services) {
    if (isServicePermitted(user, service))
         permittedServices.add(service);
}
return permittedServices;

何时首选"删除"?一个考虑因素是如果给出一个大的列表或昂贵的"添加",加上与列表大小相比只删除了几个。只进行少量删除而不是大量添加可能更有效。但在我的情况下,这种情况不值得这样优化。


当您想从列表中删除元素时,最好使用迭代器。

因为remove的源代码是

1
2
3
4
if (numMoved > 0)
    System.arraycopy(elementData, index+1, elementData, index,
             numMoved);
elementData[--size] = null;

因此,如果从列表中删除一个元素,列表将被重组,另一个元素的索引将被更改,这可能导致您想要发生的事情。


  • 试试这个2。把条件改为"冬天",你会想:
  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public static void main(String[] args) {
      Season.add("Frühling");
      Season.add("Sommer");
      Season.add("Herbst");
      Season.add("WINTER");
      for (String s : Season) {
       if(!s.equals("Sommer")) {
        System.out.println(s);
        continue;
       }
       Season.remove("Frühling");
      }
     }

    使用

    .remove()交互程序或

    使用

    CopyOnWriteArrayList