本文主要總結了Java
中遍歷集合或數組的幾種方式,並介紹了各類遍歷方式的實現原理,以及一些最佳實踐。最後介紹了Java
集合類迭代器的快速失敗(fail-fast
)機制。html
遍歷必然須要使用到循環結構,Java
中有如下幾種循環結構:java
while
語句do...while
語句for
語句for
語句對於do...while
語句,其第一個循環體是必須會執行的,這對於空集合或者空數組是不適用的。因此咱們通常不會使用do...while
語句來進行遍歷。其他三種都是咱們常常用來遍歷的語法結構。算法
for
語句和while
語句在通常狀況下能夠互相轉化,下文咱們並不將兩種語句單獨區分,會根據場景選擇使用更加簡單的方式。編程
使用循環遍歷數組下標範圍,在循環體中用下標訪問數組元素:設計模式
for (int i = 0; i < array.length; i++) {
System.out.println(array[i]);
}
複製代碼
使用加強for
循環語法結構來對數組進行遍歷:api
for (int value : array) {
System.out.println(value);
}
複製代碼
加強for
循環 其實只是一種語法糖,使用 加強for
循環 在遍歷數組時,在編譯過程會將其轉化爲 "使用下標遍歷" 的方式,在字節碼層面其實等價於第一種方式,效率上也沒有太大差異。數組
關於加強for
循環語法更詳細的介紹,請移步:Java Language Specification - 14.14.2. The enhanced for statement安全
在 面向對象編程裏,迭代器模式是一種設計模式,是一種最簡單也最多見的設計模式。迭代器模式提供了一種方法順序訪問一個集合對象中的各個元素,而又不暴露其內部的表示。Java
中也提供了對迭代器模式的支持,主要是針對Java
的各類集合類進行遍歷。markdown
Iterator
接口是Java
中對迭代器的抽象接口定義,其定義以下:數據結構
public interface Iterator<E> {
// 是否還有下一個元素
boolean hasNext();
// 返回下一個元素
E next();
// 刪除迭代過程當中最近訪問的一個元素
// 也就是在next()以後調用remove()刪除剛剛next()返回的元素
default void remove() {
throw new UnsupportedOperationException("remove");
}
}
複製代碼
Iterable
接口是在Java 1.5
中引入的,爲了用來支持加強型for
循環,只有實現了Iterable
接口的對象纔可使用加強型for
循環。Iterable
接口定義以下:
public interface Iterable<T> {
// 返回迭代器
Iterator<T> iterator();
// 使用函數式接口對加強型for循環進行包裝,能夠方便地使用lambda表達式來進行遍歷
default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
}
複製代碼
能夠看到Iterable
接口提供了forEach
方法的默認實現,函數參數是一個函數式接口action
參數來表示對遍歷到的每一個元素的操做行爲,實現邏輯是使用 加強for
循環 遍歷自身,循環中對每一個元素都應用 參數action
所表示的操做行爲。
List
表示的是一個有序的元素集合。List
接口繼承了Collection
接口,Collection
接口又繼承了Iterable
接口,其定義以下:
public interface Iterable<T> {
// 返回Iterator迭代器
Iterator<T> iterator();
}
public interface Collection<E> extends Iterable<E> {
}
public interface List<E> extends Collection<E> {
// 獲取指定位置的元素
E get(int index);
// 獲取ListIterator迭代器
ListIterator<E> listIterator();
// 從指定位置獲取ListIterator迭代器
ListIterator<E> listIterator(int index);
}
複製代碼
能夠看到List
除了能夠經過iterator()
方法得到Iterator
迭代器以外,還能夠經過listIterator()
方法得到ListIterator
迭代器。ListIterator
迭代器相比於Iterator
迭代器以外,訪問和操做元素的方法更加豐富:
Iterator
只能向後迭代,而ListIterator
能夠向兩個方向迭代Iterator
只能在迭代過程當中刪除元素,而ListIterator
能夠添加元素、刪除元素、修改元素。public interface ListIterator<E> extends Iterator<E> {
// Query Operations
// 是否還有後一個元素
boolean hasNext();
// 訪問後一個元素
E next();
// 是否還有前一個元素
boolean hasPrevious();
// 訪問前一個元素
E previous();
// 後一個元素的下標
int nextIndex();
// 前一個元素的下標
int previousIndex();
// Modification Operations
// 刪除元素
void remove();
// 修改元素
void set(E e);
// 添加元素
void add(E e);
}
複製代碼
List
接口提供了get
方法來訪問指定位置的元素,因此與遍歷數組同樣,List
也能夠經過遍歷List
下標並使用get
方法訪問元素來遍歷List
:
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
複製代碼
List
可使用繼承自Iterable
接口的iterator()
方法來得到Iterator
迭代器,使用Iterator
迭代器來遍歷List
:
Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next());
}
複製代碼
List
還可使用listIterator()
方法來得到ListIterator
迭代器,使用ListIterator
迭代器來遍歷List
:
ListIterator<Integer> listIterator = list.listIterator();
while (listIterator.hasNext()){
System.out.println(listIterator.next());
}
// ListIterator 能夠向前遍歷
listIterator = list.listIterator(list.size() - 1);
while (listIterator.hasPrevious()){
System.out.println(listIterator.previous());
}
複製代碼
咱們前面說到,實現了Iterable
接口的對象可使用加強for
循環遍歷。List
接口繼承自Iterable
接口,因此可使用加強for
循環來遍歷List
:
for (Integer value : list) {
System.out.println(value);
}
複製代碼
加強性for
循環是一種語法糖,可是與遍歷數組不同的是,使用加強型for
循環在遍歷實現了Iterable
接口的對象時,會在編譯過程當中將其轉化爲使用Iterator
迭代器進行遍歷的方式。因此這種方式本質上與上一種方式是同樣的。
關於加強for
循環語法更詳細的介紹,請移步:Java Language Specification - 14.14.2. The enhanced for statement
List接口實現了Iterable
接口,Iterable
接口中提供了forEach
方法來更加方便的遍歷集合。其參數是一個函數式接口action
,來表示對遍歷到的每一個元素的操做行爲。而且由於參數是一個函數式接口,因此咱們可使用lamdba
表達式更簡潔的表達遍歷過程。
Iterable
接口的forEach
方法的默認實現是使用加強for
循環來遍歷自身。因此若是沒有重寫forEach
方法的話,這種方式本質上與上一種方式是同樣的。
list.forEach(value -> System.out.println(value));
list.forEach(System.out::println);
複製代碼
上面幾種方式其實本質上來說只有兩種方式:
get
方法獲取集合元素 來遍歷List
。Iterator
或ListIterator
)來遍歷List
。其他方式都只不過是第二種方式的語法糖或其變種。 那麼到底應該使用哪一種方式更好呢?這取決於List
的內部實現方式。
List
的經常使用實現數據結構有兩種,數組
和鏈表
:
List
及其對應的Iterator
/ListIterator
實現來講,好比ArrayList
、Vector
。List.get()
方法、Iterator.next()
方法、ListIterator.next()
方法、ListIterator.previous()
方法的時間複雜度都爲O(1)
。因此使用下標遍歷全部元素的時間複雜度爲O(N)
,使用迭代器遍歷全部元素的時間複雜度也爲O(N)
。可是下標遍歷的方式執行的代碼更少更簡單,因此效率稍高。List
其對應的Iterator
/ListIterator
實現來講,List.get()
方法的時間複雜度爲O(N)
,Iterator.next()
方法、ListIterator.next()
方法、ListIterator.previous()
方法的時間複雜度都爲O(1)
。因此使用下標遍歷全部元素的時間複雜度爲O(N*N)
,使用迭代器遍歷全部元素的時間複雜度爲O(N)
。因此使用迭代器遍歷效率更高。Java
集合框架中,提供了一個RandomAccess
接口,該接口沒有方法,只是一個標記。一般被List
接口的實現類使用,用來標記該List
的實現類是否支持Random Access
。一個集合類實現了該接口,就意味着它支持Random Access
,按位置讀取元素的平均時間複雜度爲O(1)
,好比ArrayList
。而沒有實現該接口的,就表示不支持Random Access
,好比LinkedList
。因此推薦的作法就是,若是想要遍歷一個List
,若是其實現了RandomAccess
接口,那麼使用下標遍歷效率更高,不然的話使用迭代器遍歷效率更高。
public interface Iterable<T> {
// 返回Iterator迭代器
Iterator<T> iterator();
}
public interface Collection<E> extends Iterable<E> { }
public interface Set<E> extends Collection<E> { }
複製代碼
相較於List
,Set
是無序的,因此Set
沒有經過下標獲取元素的get
方法,也就沒辦法使用下標來遍歷。Set
也沒有相似ListIterator
同樣特殊的迭代器。因此遍歷Set
只能使用Iterator
迭代器來遍歷。下面三種方式其實本質上都是使用Iterator
迭代器來遍歷,後兩種方式只是第一種方式的語法糖或者變種。
Set
接口一樣繼承了Iterable
的iterator()
方法,可使用其返回的Iterator
迭代器來遍歷Set
:
Iterator<Integer> iterator = set.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next());
}
複製代碼
Set
接口實現了Iterable
接口,因此也可使用加強型for
循環來遍歷Set
:
for (Integer value : set) {
System.out.println(value);
}
複製代碼
Set
接口實現了Iterable
接口,因此也可使用forEach
方法來遍歷Set
:
set.forEach(value -> System.out.println(value));
set.forEach(System.out::println);
複製代碼
不一樣於List
,Map
並非一組元素的集合,而是一組鍵值對,因此Map
沒有繼承Collection
、Iterable
等其餘接口。
public interface Map<K,V> {
// 返回Map中鍵的集合
Set<K> keySet();
// 返回Map中值的集合
Collection<V> values();
// 返回Map中鍵值對的集合
Set<Map.Entry<K, V>> entrySet();
// 相似於Iterable接口的forEach方法
default void forEach(BiConsumer<? super K, ? super V> action) {
Objects.requireNonNull(action);
for (Map.Entry<K, V> entry : entrySet()) {
K k;
V v;
try {
k = entry.getKey();
v = entry.getValue();
} catch(IllegalStateException ise) {
// this usually means the entry is no longer in the map.
throw new ConcurrentModificationException(ise);
}
action.accept(k, v);
}
}
}
複製代碼
Map
提供了keySet()
、values()
、entrySet()
方法來分別獲取Map
的 鍵集合、值集合、鍵值對集合。而且提供了相似於Iterable
的forEach
方法及其默認實現。
遍歷Map
能夠經過先獲取其 鍵集合、值集合、鍵值對集合,而後根據返回的集合類型選擇不一樣的遍歷方式。
同時,Map
也提供了相似於Iterable
的forEach
方法,參數action
是一個函數式接口,指定了對於每個鍵值對的操做行爲,實現邏輯是使用加強for
循環遍歷Map
的entrySet()
方法的返回值,對於每個遍歷到的每個鍵值對,應用參數action
表明的操做行爲。
wikipedia上對快速失敗(fail-fast)
的介紹是:
In systems design, a fail-fast system is one which immediately reports at its interface any condition that is likely to indicate a failure. Fail-fast systems are usually designed to stop normal operation rather than attempt to continue a possibly flawed process.
簡單來講就是系統運行中,若是有錯誤發生,那麼系統當即結束,而不是繼續冒不肯定的風險繼續執行,這種設計就是"快速失敗"。
Java
集合框架中的一些集合類的迭代器也是被設計爲快速失敗
的。集合迭代器中的快速失敗機制是說:
在使用迭代器遍歷集合時,若是迭代器建立以後,經過 除了迭代器提供的修改方法以外 的其餘方式對集合進行告終構性修改(添加、刪除元素等),那麼迭代器應該拋出一個
ConcurrentModificationException
異常,表示在這次遍歷中集合發生了"併發修改",應該提早終止迭代過程。由於在迭代器遍歷集合的過程當中,若是有別的行爲改變了集合自己的結構,那麼迭代器以後的行爲可能就是不符合預期的,可能會出現錯誤的結果,因此提早檢測並拋出異常是一個更好的作法。
Java
中大部分基本的集合類的迭代器都實現了快速失敗機制,包括ArrayList
,LinkedList
,Vector
,HashMap
,HashSet
等等。可是對於併發集合類,例如ConcurrentHashMap
,CopyOnWriteArrayList
等,這些類自己就是設計來支持併發的,是線程安全的,因此也不存在快速失敗這一說。
以ArrayList
爲例,ArrayList (Java Platform SE 8 )中對fail-fast
的介紹以下:
The iterators returned by this class's iterator and listIterator methods are fail-fast: if the list is structurally modified at any time after the iterator is created, in any way except through the iterator's own remove or add methods, the iterator will throw a ConcurrentModificationException. Thus, in the face of concurrent modification, the iterator fails quickly and cleanly, rather than risking arbitrary, non-deterministic behavior at an undetermined time in the future.
ArrayList
的iterator
和listIterator
方法返回的迭代器都是fail-fast
的:若是在迭代器建立以後,經過除了迭代器自身的add
/remove
方法以外的其餘方式,對ArrayList
進行告終構性修改(添加、刪除元素等),那麼該迭代器應該拋出一個ConcurrentModificationException
異常。因此,迭代器在面對併發修改時,迭代器將快速而乾淨地失敗,而不是冒着在將來不肯定的時間發生不肯定行爲的風險繼續執行。
若是看過阿里的《JAVA開發手冊》,會知道里面有這一條規範:
【強制】不要在
foreach
循環裏進行元素的remove
/add
操做。remove
元素請使用Iterator
方式,若是併發操做,須要對Iterator
對象加鎖。
上面說的foreach
循環指的上面咱們提到的使用加強for
循環進行遍歷的方式。這種方式本質上使用的是迭代器方式。因此更明確一點,這個規範其實就是在說:
在使用
Iterator
迭代器遍歷集合過程當中,不要經過集合自己的remove
/add
方法來進行元素的remove
/add
操做,remove
請使用Iterator
提供的remove
方法。
正確方式:
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()) {
Integer item = iterator.next();
if (item == 1) {
iterator.remove();
}
}
list.forEach(System.out::println); // 輸出 2 3
複製代碼
錯誤方式:
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
for (Integer item : list) { // 會在這裏拋出異常
if (item == 1) {
list.remove(item); // 使用 list.remove 刪除
}
}
list.forEach(System.out::println);
複製代碼
上述代碼會在第五行拋出java.util.ConcurrentModificationException
異常。由於其本質仍是使用迭代器遍歷,因此爲了方便理解其緣由,咱們將上述方式改寫爲:
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()) {
Integer item = iterator.next(); // 會在這裏拋出異常
if (item == 1) {
list.remove(item); // 使用 list.remove 刪除
}
}
list.forEach(System.out::println);
複製代碼
這段代碼會在第7
行拋出java.util.ConcurrentModificationException
異常。與第一種方式的差異僅僅在於使用了List.remove
方法而不是Iterator.remove
方法。
以ArrayList
爲例來講明快速失敗機制的實現原理,其餘類的實現方式也是大體相同的。
ArrayList
中有一個屬性modCount
,是一個記錄ArrayList
修改次數的計數器。ArrayList
的iterator
/listIterator
方法被調用時,會建立出Iteartor
/ListIterator
的實現類 ArrayList.Itr
/ArrayList.ListItr
迭代器對象,其中有一個屬性expectedModCount
。當迭代器被建立時,ArrayList
自己的modCount
將被複制給Itr
/ListItr
中的expectedModCount
屬性。remove
/add
方法增刪元素時,在修改ArrayList
的modCount
以後,還會將其值複製給迭代器自身的expectedModCount
。而經過ArrayList
的remove
/add
方法增刪元素時,僅僅修改了ArrayList
的modCount
。next
/remove
/add
/set
操做時是會檢查迭代器自身的expectedModCount
與ArrayList
的modCount
是否相等,若是不相等則會拋出ConcurrentModificationException
異常。因此在迭代過程當中,若是隻使用迭代器的remove
/add
方法增刪元素,是不會出現問題的,由於在增刪元素以後迭代器始終會將ArrayList
的modCount
值賦值給迭代器自身的expectedModCount
,因此下次迭代二者必定相等。而若是迭代過程當中使用了ArrayList
的remove
/add
方法增刪元素,或者有另一個迭代器進行了增刪元素,就會形成ArrayList
中的modCount
與迭代器中的expectedModCount
不一致,拋出ConcurrentModificationException
異常。
快速失敗機制也不可以保證百分之百生效,例如,在下面這段代碼中,使用迭代器遍歷ArrayList
過程當中,使用ArrayList
的remove
方法刪除倒數第二個元素,程序可以正確刪除,並不會像咱們上面所說的拋出ConcurrentModificationException
異常。
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()) {
Integer item = iterator.next();
if (item == 2) {
list.remove(item);
}
}
list.forEach(System.out::println); // 輸出 1 3
複製代碼
這個"bug
"發生的緣由在於,在第二次執行iterator.next()
後,迭代器記錄的下一次將要訪問的下標應該是2
,而在執行list.remove()
刪除元素後,list
的size
變爲了2
,因此在下次執行iterator.hasNext()
時認爲已經沒有元素要繼續迭代了,返回false
,結束循環。
因此fail-fast
機制並不可以徹底保證全部的併發修改的狀況都拋出ConcurrentModificationException
異常,在程序中也不該該依賴於這個異常信息。 在ArrayList (Java Platform SE 8 )中也指出了fail-fast
這一性質:
Note that the fail-fast behavior of an iterator cannot be guaranteed as it is, generally speaking, impossible to make any hard guarantees in the presence of unsynchronized concurrent modification. Fail-fast iterators throw ConcurrentModificationException on a best-effort basis. Therefore, it would be wrong to write a program that depended on this exception for its correctness: the fail-fast behavior of iterators should be used only to detect bugs.