Iterator 與 ListIterator
凡是實現 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");
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"
。原因是開始時 List
的 size
為 4
,從前往後,迴圈到了索引 #1
,發現符合條件,於是刪除了 #1
的元素。此時 List
的 size
變為 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
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
,則使用方法三; - 如果鍵值都需要,則使用方法一。