Java 集合(2)之 Iterator 迭代器

Iterator 與 ListIterator

凡是實現 Collection 接口的集合類都有一個 iterator 方法,會返回一個實現了 Iterator 接口的對象,用於遍歷集合。Iterator 接口主要有三個方法,分別是 hasNextnextremove 方法。java

ListIterator 繼承自 Iterator,專門用於實現 List 接口對象,除了 Iterator 接口的方法外,還有其餘幾個方法。安全

基於順序存儲集合的 Iterator 能夠直接按位置訪問數據。基於鏈式存儲集合的 Iterator,通常都是須要保存當前遍歷的位置,而後根據當前位置來向前或者向後移動指針。多線程

IteratorListIterator 的區別:併發

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

快速失敗(fail—fast)

快速失敗機制(fail—fast)就是在使用迭代器遍歷一個集合對象時,若是遍歷過程當中對集合進行修改(增刪改),則會拋出 ConcurrentModificationException 異常。性能

例如如下代碼,就會拋出 ConcurrentModificationExceptionspa

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 包下的集合類都採用快速失敗機制,因此在多線程下,不能發生併發修改,也就是在迭代過程當中不能被修改。code

安全失敗(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,則使用方法三;
  • 若是鍵值都須要,則使用方法一。
相關文章
相關標籤/搜索