從迴圈內的 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));