Java 集合(2)之 Iterator 迭代器

TimberLiu發表於2019-03-02

Iterator 與 ListIterator

凡是實現 Collection 介面的集合類都有一個 iterator 方法,會返回一個實現了 Iterator 介面的物件,用於遍歷集合。Iterator 介面主要有三個方法,分別是 hasNextnextremove 方法。

ListIterator 繼承自 Iterator,專門用於實現 List 介面物件,除了 Iterator 介面的方法外,還有其他幾個方法。

基於順序儲存集合的 Iterator 可以直接按位置訪問資料。基於鏈式儲存集合的 Iterator,一般都是需要儲存當前遍歷的位置,然後根據當前位置來向前或者向後移動指標。

IteratorListIterator 的區別:

  • Iterator 可用於遍歷 SetListListIterator 只可用於遍歷 List
  • Iterator 只能向後遍歷;ListIterator 可向前或向後遍歷。
  • ListIterator 實現了 Iterator 的介面,並增加了
    addsethasPreviouspreviouspreviousIndexnextIndex 方法。

快速失敗(fail—fast)

快速失敗機制(fail—fast)就是在使用迭代器遍歷一個集合物件時,如果遍歷過程中對集合進行修改(增刪改),則會丟擲 ConcurrentModificationException 異常。

例如以下程式碼,就會丟擲 ConcurrentModificationException

List<String> stringList = new ArrayList<>();
stringList.add("abc");
stringList.add("def");

Iterator<String> iterator = stringList.iterator();
while (iterator.hasNext()) {
    System.out.println(iterator.next());
    stringList.add("ghi");
}
複製程式碼

檢視 ArrayList 原始碼,就可以知道為什麼會丟擲異常。原因是在 ArrayList 類的內部類迭代器 Itr 中有一個 expectedModCount 變數。在 AbstracList 抽象類有一個 modCount 變數,集合在被遍歷期間如果內容發生變化,就會改變 modCount 的值。每當迭代器使用 next() 遍歷下一個元素之前,都會檢測 modCount 變數是否等於 expectedmodCount ,如果相等就繼續遍歷;否則就會丟擲異常。

final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}
複製程式碼

注意:這裡異常的丟擲條件是檢測到 modCount != expectedmodCount。如果集合發生變化時將 modCount 的值又剛好設定為 expectedmodCount,那麼就不會丟擲異常。因此,不能依賴於這個異常是否丟擲而進行併發操作,這個異常只建議使用於檢測併發修改的 bug

java.util 包下的集合類都採用快速失敗機制,所以在多執行緒下,不能發生併發修改,也就是在迭代過程中不能被修改。

安全失敗(fail—safe)

採用安全失敗機制(fail—safe)的集合類,在遍歷集合時不是直接訪問原有集合,而是先將原有集合的內容複製一份,然後在拷貝的集合上進行遍歷。由於是對拷貝的集合進行遍歷,所以在遍歷過程中對原集合的修改並不會被迭代器檢測到,所以不會丟擲 ConcurrentModificationException 異常。

雖然基於拷貝內容的安全失敗機制避免了 ConcurrentModificationException,但是迭代器並不能訪問到修改後的內容,而仍然是開始遍歷那一刻拿到的集合拷貝。

java.util.concurrent 包下的集合都採用安全失敗機制,所以可以在多執行緒場景下進行併發使用和修改操作。

如何在遍歷集合的同時刪除元素

在遍歷集合時,正確的刪除方式有以下幾種:

普通 for 迴圈

在使用普通 for 迴圈時,如果從前往後遍歷:

ArrayList<String> stringList = new ArrayList<>();
stringList.add("abc");
stringList.add("def");
stringList.add("def");
stringList.add("ghi");

for (int i = 0;i < stringList.size(); i++) {
    String str = stringList.get(i);
    if ("def".equals(str)) {
        stringList.remove(str);
    }
}
複製程式碼

列印結果為:

abc def ghi
複製程式碼

可以看到,這裡跳過了第二個 "def"。原因是開始時 Listsize4,從前往後,迴圈到了索引 #1,發現符合條件,於是刪除了 #1 的元素。此時 Listsize 變為 3,索引 #1 就指向了之前 #2 的元素(就是 #2 的元素移動了 #1#3 移動到了 #2)。

而下一次迴圈會從索引 #2 開始,檢視的是刪除之前 #3 的元素,於是之前 #2 的元素(左移到了 #1)就被跳過了。

而如果從後往前遍歷,就可以避免元素移動造成的影響。

ArrayList<String> stringList = new ArrayList<>();
stringList.add("abc");
stringList.add("def");
stringList.add("def");
stringList.add("ghi");

for (int i = stringList.size() - 1;i >= 0; i--) {
    String str = stringList.get(i);
    if ("abc".equals(str)) {
        stringList.remove(str);
    }
}
// abc ghi
複製程式碼

foreach 刪除後跳出迴圈

在使用 foreach 迭代器遍歷集合時,在刪除元素後使用 break 跳出迴圈,則不會觸發 fail-fast

for (String str : stringList) {
    if ("abc".equals(str)) {
        stringList.remove(str);
        break;
    }
}
複製程式碼

使用迭代器

使用迭代器自帶的 remove 方法刪除元素,也不會丟擲異常。

Iterator<String> iterator = stringList.iterator();
while (iterator.hasNext()) {
    String str = iterator.next();
    if ("abc".equals(str)) {
        iterator.remove();  // 這裡是 iterator,而不是 stringList
    }
}  
複製程式碼

Enumeration

EnumerationJDK1.0 引入的介面,為集合提供遍歷的介面,使用它的集合包括 VectorHashTable 等。Enumeration 迭代器不支援 fail-fast 機制。

它只有兩個介面方法:hasMoreElementsnextElement 用來判斷是否有元素和獲取元素,但不能對資料進行修改。

但需要注意的是 Enumeration 迭代器只能遍歷 VectorHashTable 這種古老的集合,因此通常情況下不要使用。

Java中遍歷 Map 的幾種方式

方法一 在 for-each 迴圈中使用 entries 來遍歷

這是最常見的,並且在大多數情況下也是最可取的遍歷方式,在鍵和值都需要時使用。

Map<Integer, Integer> map = new HashMap<>();  
for (Map.Entry<Integer, Integer> entry : map.entrySet()) {  
    System.out.println("Key = " + entry.getKey() + ", Value = " + entry.getValue());  
}  
複製程式碼

注意:如果遍歷一個空 map 物件,for-each 迴圈將丟擲 NullPointerException,因此在遍歷前應該檢查是否為空引用。

方法二 在 for-each 迴圈中遍歷 keys 或 values

如果只需要 map 中的鍵或者值,可以通過 keySetvalues 來實現遍歷,而不是用 entrySet

Map<Integer, Integer> map = new HashMap<Integer, Integer>();  

//遍歷 map 中的鍵  
for (Integer key : map.keySet()) {  
    System.out.println("Key = " + key);  
}  

//遍歷 map 中的值  
for (Integer value : map.values()) {  
    System.out.println("Value = " + value);  
}  
複製程式碼

該方法比 entrySet 遍歷在效能上稍好,而且程式碼更加乾淨。

方法三 使用 Iterator 遍歷

Map<Integer, Integer> map = new HashMap<Integer, Integer>();  

Iterator<Map.Entry<Integer, Integer>> entries = map.entrySet().iterator();  
  
while (entries.hasNext()) {  
    Map.Entry<Integer, Integer> entry = entries.next();  
    System.out.println("Key = " + entry.getKey() + ", Value = " + entry.getValue());  
}  
複製程式碼

這種方式看起來冗餘卻有其優點所在,可以在遍歷時呼叫 iterator.remove() 來刪除 entries,另兩個方法則不能。

從效能方面看,該方法類同於 for-each 遍歷(即方法二)的效能。

總結

  • 如果僅需要鍵(keys)或值(values),則使用方法二;
  • 如果需要在遍歷時刪除 entries,則使用方法三;
  • 如果鍵值都需要,則使用方法一。

相關文章