Java 集合(2)之 Iterator 迭代器
凡是實現Collection
介面的集合類都有一個iterator
方法,會返回一個實現了Iterator
介面的物件,用於遍歷集合。Iterator
介面主要有三個方法,分別是hasNext
、next
、remove
方法。
ListIterator
繼承自Iterator
,專門用於實現List
介面物件,除了Iterator
介面的方法外,還有其他幾個方法。
基於順序儲存集合的Iterator
可以直接按位置訪問資料。基於鏈式儲存集合的Iterator
,一般都是需要儲存當前遍歷的位置,然後根據當前位置來向前或者向後移動指標。
Iterator
與ListIterator
的區別:
-
Iterator
可用於遍歷Set
、List
;ListIterator
只可用於遍歷List
。 -
Iterator
只能向後遍歷;ListIterator
可向前或向後遍歷。 -
ListIterator
實現了Iterator
的介面,並增加了add
、set
、hasPrevious
、previous
、previousIndex
、nextIndex
方法。
快速失敗(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"); for (int i = 0;i < stringList.size(); i++) { String str = stringList.get(i); if ("abc".equals(str)) { stringList.remove(str); break; } } 複製程式碼
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 break; } } 複製程式碼
Enumeration
Enumeration
是JDK1.0
引入的介面,為集合提供遍歷的介面,使用它的集合包括Vector
、HashTable
等。Enumeration
迭代器不支援fail-fast
機制。
它只有兩個介面方法:hasMoreElements
、nextElement
用來判斷是否有元素和獲取元素,但不能對資料進行修改。
但需要注意的是Enumeration
迭代器只能遍歷Vector
、HashTable
這種古老的集合,因此通常情況下不要使用。
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
中的鍵或者值,可以通過keySet
或values
來實現遍歷,而不是用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
,則使用方法三; - 如果鍵值都需要,則使用方法一。