Java中的遍歷(遍歷集合或數組的幾種方式)

本文主要總結了Java中遍歷集合或數組的幾種方式,並介紹了各類遍歷方式的實現原理,以及一些最佳實踐。最後介紹了Java集合類迭代器的快速失敗(fail-fast)機制。html

Java中的循環結構

遍歷必然須要使用到循環結構,Java中有如下幾種循環結構:java

  1. while語句
  2. do...while語句
  3. 基本for語句
  4. 加強for語句

對於do...while語句,其第一個循環體是必須會執行的,這對於空集合或者空數組是不適用的。因此咱們通常不會使用do...while語句來進行遍歷。其他三種都是咱們常常用來遍歷的語法結構。算法

for語句和while語句在通常狀況下能夠互相轉化,下文咱們並不將兩種語句單獨區分,會根據場景選擇使用更加簡單的方式。編程

遍歷數組

1. 使用下標遍歷

使用循環遍歷數組下標範圍,在循環體中用下標訪問數組元素:設計模式

for (int i = 0; i < array.length; i++) {
    System.out.println(array[i]);
}
複製代碼

2. 加強for循環

使用加強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中也提供了對迭代器模式的支持,主要是針對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接口的定義

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迭代器以外,訪問和操做元素的方法更加豐富:

  1. Iterator只能向後迭代,而ListIterator能夠向兩個方向迭代
  2. 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的遍歷方法

1. 使用下標遍歷

List接口提供了get方法來訪問指定位置的元素,因此與遍歷數組同樣,List也能夠經過遍歷List下標並使用get方法訪問元素來遍歷List

for (int i = 0; i < list.size(); i++) {
    System.out.println(list.get(i));
}
複製代碼

2. 使用Iterator迭代器

List可使用繼承自Iterable接口的iterator()方法來得到Iterator迭代器,使用Iterator迭代器來遍歷List

Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()){
    System.out.println(iterator.next());
}
複製代碼

3. 使用ListIterator迭代器

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());
}
複製代碼

4. 加強for循環

咱們前面說到,實現了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

5. Iterable接口的forEach方法

List接口實現了Iterable接口,Iterable接口中提供了forEach方法來更加方便的遍歷集合。其參數是一個函數式接口action,來表示對遍歷到的每一個元素的操做行爲。而且由於參數是一個函數式接口,因此咱們可使用lamdba表達式更簡潔的表達遍歷過程。

Iterable接口的forEach方法的默認實現是使用加強for循環來遍歷自身。因此若是沒有重寫forEach方法的話,這種方式本質上與上一種方式是同樣的。

list.forEach(value -> System.out.println(value));
list.forEach(System.out::println);
複製代碼

最佳實踐

上面幾種方式其實本質上來說只有兩種方式:

  1. 使用循環遍歷集合的下標範圍,配合get方法獲取集合元素 來遍歷List
  2. 使用迭代器(IteratorListIterator)來遍歷List

其他方式都只不過是第二種方式的語法糖或其變種。 那麼到底應該使用哪一種方式更好呢?這取決於List的內部實現方式。

List的經常使用實現數據結構有兩種,數組鏈表

  1. 對於數組實現的List及其對應的Iterator/ListIterator實現來講,好比ArrayListVectorList.get()方法、Iterator.next()方法、ListIterator.next()方法、ListIterator.previous()方法的時間複雜度都爲O(1)。因此使用下標遍歷全部元素的時間複雜度爲O(N),使用迭代器遍歷全部元素的時間複雜度也爲O(N)。可是下標遍歷的方式執行的代碼更少更簡單,因此效率稍高。
  2. 而對於鏈表實現的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接口,那麼使用下標遍歷效率更高,不然的話使用迭代器遍歷效率更高

遍歷Set

Set接口的定義

public interface Iterable<T> {
    // 返回Iterator迭代器
    Iterator<T> iterator();
}
public interface Collection<E> extends Iterable<E> { }
public interface Set<E> extends Collection<E> { }
複製代碼

Set的遍歷方法

相較於ListSet是無序的,因此Set沒有經過下標獲取元素的get方法,也就沒辦法使用下標來遍歷。Set也沒有相似ListIterator同樣特殊的迭代器。因此遍歷Set只能使用Iterator迭代器來遍歷。下面三種方式其實本質上都是使用Iterator迭代器來遍歷,後兩種方式只是第一種方式的語法糖或者變種。

1. 使用Iterator迭代器

Set接口一樣繼承了Iterableiterator()方法,可使用其返回的Iterator迭代器來遍歷Set

Iterator<Integer> iterator = set.iterator();
while (iterator.hasNext()){
    System.out.println(iterator.next());
}
複製代碼

2. 加強for循環

Set接口實現了Iterable接口,因此也可使用加強型for循環來遍歷Set

for (Integer value : set) {
    System.out.println(value);
}
複製代碼

3. Iterable接口的forEach方法

Set接口實現了Iterable接口,因此也可使用forEach方法來遍歷Set

set.forEach(value -> System.out.println(value));
set.forEach(System.out::println);
複製代碼

遍歷Map

不一樣於ListMap並非一組元素的集合,而是一組鍵值對,因此Map沒有繼承CollectionIterable等其餘接口。

Map接口的定義

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的 鍵集合、值集合、鍵值對集合。而且提供了相似於IterableforEach方法及其默認實現。

Map的遍歷方法

遍歷Map能夠經過先獲取其 鍵集合、值集合、鍵值對集合,而後根據返回的集合類型選擇不一樣的遍歷方式。

同時,Map也提供了相似於IterableforEach方法,參數action是一個函數式接口,指定了對於每個鍵值對的操做行爲,實現邏輯是使用加強for循環遍歷MapentrySet()方法的返回值,對於每個遍歷到的每個鍵值對,應用參數action表明的操做行爲。

Java集合中的快速失敗機制

何爲快速失敗

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集合迭代器的快速失敗機制

Java集合框架中的一些集合類的迭代器也是被設計爲快速失敗的。集合迭代器中的快速失敗機制是說:

在使用迭代器遍歷集合時,若是迭代器建立以後,經過 除了迭代器提供的修改方法以外 的其餘方式對集合進行告終構性修改(添加、刪除元素等),那麼迭代器應該拋出一個ConcurrentModificationException異常,表示在這次遍歷中集合發生了"併發修改",應該提早終止迭代過程。由於在迭代器遍歷集合的過程當中,若是有別的行爲改變了集合自己的結構,那麼迭代器以後的行爲可能就是不符合預期的,可能會出現錯誤的結果,因此提早檢測並拋出異常是一個更好的作法。

Java中大部分基本的集合類的迭代器都實現了快速失敗機制,包括ArrayListLinkedListVectorHashMapHashSet等等。可是對於併發集合類,例如ConcurrentHashMapCopyOnWriteArrayList等,這些類自己就是設計來支持併發的,是線程安全的,因此也不存在快速失敗這一說。

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.

ArrayListiteratorlistIterator方法返回的迭代器都是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爲例來講明快速失敗機制的實現原理,其餘類的實現方式也是大體相同的。

  1. ArrayList中有一個屬性modCount,是一個記錄ArrayList修改次數的計數器。
  2. ArrayListiterator/listIterator方法被調用時,會建立出Iteartor/ListIterator的實現類 ArrayList.Itr/ArrayList.ListItr迭代器對象,其中有一個屬性expectedModCount。當迭代器被建立時,ArrayList自己的modCount將被複制給Itr/ListItr中的expectedModCount屬性。
  3. 當使用迭代器的remove/add方法增刪元素時,在修改ArrayListmodCount以後,還會將其值複製給迭代器自身的expectedModCount。而經過ArrayListremove/add方法增刪元素時,僅僅修改了ArrayListmodCount
  4. 當迭代器執行next/remove/add/set操做時是會檢查迭代器自身的expectedModCountArrayListmodCount是否相等,若是不相等則會拋出ConcurrentModificationException異常。

因此在迭代過程當中,若是隻使用迭代器的remove/add方法增刪元素,是不會出現問題的,由於在增刪元素以後迭代器始終會將ArrayListmodCount值賦值給迭代器自身的expectedModCount,因此下次迭代二者必定相等。而若是迭代過程當中使用了ArrayListremove/add方法增刪元素,或者有另一個迭代器進行了增刪元素,就會形成ArrayList中的modCount與迭代器中的expectedModCount不一致,拋出ConcurrentModificationException異常。

快速失敗的"bug"

快速失敗機制也不可以保證百分之百生效,例如,在下面這段代碼中,使用迭代器遍歷ArrayList過程當中,使用ArrayListremove方法刪除倒數第二個元素,程序可以正確刪除,並不會像咱們上面所說的拋出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()刪除元素後,listsize變爲了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.

參考

  1. The Java Language Specification, Java SE 8 Edition
  2. Java遍歷集合的幾種方法分析(實現原理、算法性能、適用場合)
  3. ArrayList (Java Platform SE 8 )
相關文章
相關標籤/搜索