凡是實現 Collection
接口的集合類都有一個 iterator
方法,會返回一個實現了 Iterator
接口的對象,用於遍歷集合。Iterator
接口主要有三個方法,分別是 hasNext
、next
、remove
方法。java
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
)就是在使用迭代器遍歷一個集合對象時,若是遍歷過程當中對集合進行修改(增刪改),則會拋出 ConcurrentModificationException
異常。性能
例如如下代碼,就會拋出 ConcurrentModificationException
:spa
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
)的集合類,在遍歷集合時不是直接訪問原有集合,而是先將原有集合的內容複製一份,而後在拷貝的集合上進行遍歷。因爲是對拷貝的集合進行遍歷,因此在遍歷過程當中對原集合的修改並不會被迭代器檢測到,因此不會拋出 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
是 JDK1.0
引入的接口,爲集合提供遍歷的接口,使用它的集合包括 Vector
、HashTable
等。Enumeration
迭代器不支持 fail-fast
機制。
它只有兩個接口方法:hasMoreElements
、nextElement
用來判斷是否有元素和獲取元素,但不能對數據進行修改。
但須要注意的是 Enumeration
迭代器只能遍歷 Vector
、HashTable
這種古老的集合,所以一般狀況下不要使用。
方法一 在 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
,則使用方法三;