从循环内的 List 中删除项目

在循环内从列表中删除项目很棘手,这是因为列表的索引和长度发生了变化。

给出以下列表,这里有一些示例会给出意想不到的结果,有些会给出正确的结果。

List<String> fruits = new ArrayList<String>();
fruits.add("Apple");
fruits.add("Banana");
fruits.add("Strawberry");

不正确

删除 for 语句的迭代跳过香蕉

代码示例只会打印 AppleStrawberryBanana 被跳过,因为一旦 Apple 被删除它会移动到索引 0,但同时 i 会增加到 1

for (int i = 0; i < fruits.size(); i++) {
    System.out.println (fruits.get(i)); 
    if ("Apple".equals(fruits.get(i))) {
         fruits.remove(i);
    }     
}

在增强的 for 语句中删除引发异常:

因为迭代收集并同时修改它。

抛出:java.util.ConcurrentModificationException

for (String fruit : fruits) { 
    System.out.println(fruit);
    if ("Apple".equals(fruit)) {
        fruits.remove(fruit);
    }
}

正确

使用 Iterator 在 while 循环中删除

Iterator<String> fruitIterator = fruits.iterator();
while(fruitIterator.hasNext()) {     
    String fruit = fruitIterator.next();     
    System.out.println(fruit);
    if ("Apple".equals(fruit)) {
        fruitIterator.remove();
    } 
}

Iterator 接口有一个 remove() 方法,仅适用于这种情况。但是,此方法在文档中标记为可选 ,它可能会抛出一个 UnsupportedOperationException

抛出:UnsupportedOperationException - 如果此迭代器不支持 remove 操作

因此,建议检查文档以确保支持此操作(实际上,除非集合是通过第三方库获得的不可变集合或使用 Collections.unmodifiable...() 方法之一,否则几乎总是支持该操作)。

当使用 Iterator 时,当 ListmodCount 从创建 Iterator 时改变时,会抛出 ConcurrentModificationException。这可能发生在同一个线程或共享相同列表的多线程应用程序中。

modCount 是一个 int 变量,它计算此列表在结构上被修改的次数。结构变化实质上意味着在 Collection 对象上调用 add()remove() 操作(Iterator 所做的更改不计算在内)。创建 Iterator 时,它会存储此 modCount,并在 List 的每次迭代中检查当前 modCount 是否与创建 Iterator 时相同。如果 modCount 值有变化,它会抛出一个 ConcurrentModificationException

因此,对于上面声明的列表,下面的操作不会抛出任何异常:

Iterator<String> fruitIterator = fruits.iterator();
fruits.set(0, "Watermelon");
while(fruitIterator.hasNext()){
    System.out.println(fruitIterator.next());
}

但是在初始化 Iterator 之后向 List 添加一个新元素会抛出一个 ConcurrentModificationException

Iterator<String> fruitIterator = fruits.iterator();
fruits.add("Watermelon");
while(fruitIterator.hasNext()){
    System.out.println(fruitIterator.next());    //ConcurrentModificationException here
}

向后迭代

for (int i = (fruits.size() - 1); i >=0; i--) {
    System.out.println (fruits.get(i));
    if ("Apple".equals(fruits.get(i))) {
         fruits.remove(i);
    }
}

这不会跳过任何事情。这种方法的缺点是输出是反向的。但是,在大多数情况下,你删除无关紧要的项目。你永远不应该用 LinkedList 这样做。

向前迭代,调整循环索引

for (int i = 0; i < fruits.size(); i++) {
    System.out.println (fruits.get(i)); 
    if ("Apple".equals(fruits.get(i))) {
         fruits.remove(i);
         i--;
    }     
}

这不会跳过任何事情。当从 List 中移除 ith 元素时,最初位于 index i+1 的元素成为新的 ith 元素。因此,循环可以减少 i,以便下一次迭代处理下一个元素,而不会跳过。

使用应该删除列表

ArrayList shouldBeRemoved = new ArrayList();
for (String str : currentArrayList) {
    if (condition) {
        shouldBeRemoved.add(str);
    }
}
currentArrayList.removeAll(shouldBeRemoved);

此解决方案使开发人员能够以更清洁的方式检查是否删除了正确的元素。

Version => Java SE 8

在 Java 8 中,以下替代方案是可能的。如果不必在循环中进行移除,则这些更干净且更直接。

过滤流

List 可以流式传输和过滤。可以使用适当的过滤器来移除所有不需要的元素。

List<String> filteredList = 
    fruits.stream().filter(p -> !"Apple".equals(p)).collect(Collectors.toList());

请注意,与此处的所有其他示例不同,此示例生成一个新的 List 实例并保持原始 List 不变。

使用 removeIf

如果只需要删除一组项目,则可以节省构建流的开销。

fruits.removeIf(p -> "Apple".equals(p));