Java 基礎(二)集合原始碼解析 Iterator

diamond_lin發表於2017-09-24

首先,在探索集合之前,我們先來思考一個問題,集合是什麼?

針對一個特定的問題,如果事先不知道需要多少個物件,或者它們的持續時間有多長,那麼也不知道如何儲存那些物件。既然如此,怎樣才能知道那些物件要求多說空間呢?事先上根本無法提前知道,除非進入執行期。
在物件導向的設計中,大多數問題的解決辦法似乎都有些輕率——只是簡單地建立另一種型別的物件。用於解決特定問題的新型物件容納了指向其他物件的引用。當然,也可以用陣列來做同樣的事情,那是大多數語言都具有的一種功能。 但不能只看到這一點。這種新物件通常叫作“集合”(亦叫作一個“容器”)。在 需要的時候,集合會自動擴充自己,以便適應我們在其中置入的任何東西。所以 我們事先不必知道要在一個集合裡容下多少東西。只需建立一個集合,以後的工作讓它自己負責好了。

上文摘抄自《Thinking in Java》,集合解決的問題是,在編譯期間不知道要多少個物件,但是陣列必須在申明的時候明確指明陣列長度,如果食用陣列,申請太多的空間就會造成資源浪費,如果申請太少空間,就不夠用。所以引出了一個概念叫“容器”,來解決這個問題,這個容器就是我們今天要研究的物件--“集合”。

我們先來看一下類關係圖~

Java 提供的集合都在 Java.utils 包下,集合主要分兩類,Collection 和 Map。我們用到的各種型別的集合,都是實現自這兩個介面。集合的實現類有很多,開發過程中,我們需要根據不同的需求,選擇合適的集合設計,以便高效率的解決我們的實際問題。至於什麼場景用哪一種型別的容器,使用這種容器能帶來哪些好處,這就是我們要研究的核心點,也是我們用好 Java 集合的精髓。

磨刀不誤砍柴工,我們在探索集合的架構設計之前,我們先來研究一下Iterator。

Iterator

Iterator :[計]迭代器,迭代程式

迭代器,這裡用到的就是設計模式中的迭代器模式。

迭代器模式
定義:提供一種方法訪問一個容器物件中各個元素,而又不暴露該物件的內部細節。

這裡我們的重點不是迭代器模式,對“迭代器模式”感興趣的童鞋可以自行去了解一波。

先來看看介面 Iterator 的設計。

public interface Iterator<E> {
    boolean hasNext();

    E next();

    default void remove() {
        throw new UnsupportedOperationException("remove");
    }

    default void forEachRemaining(Consumer<? super E> var1) {
        Objects.requireNonNull(var1);

        while(this.hasNext()) {
            var1.accept(this.next());
        }
    }
}複製程式碼

一共四個方法,其中hasNext()和 next()方法是迭代必須方法。remove()和forEachRemaining()方法有預設實現,小夥伴不要糾結介面怎麼會有預設實現方法,這是 Java 8 的新特性。

  • hasNext():是否有下一個元素
  • next():獲取下一個元素
  • remove():刪除當前元素,非必須的方法,有需要可重寫實現。
  • forEachRemaining():給剩下來所有元素做了一個自定義的相同操作。非必須的方法,有需要可重寫實現。

#fail-fast 與 ConcurrentModificationException

fail-fast:是java集合(Collection)中的一種錯誤機制。當多個執行緒對同一個集合的內容進行操作時,就可能會產生fail-fast事件。
ConcurrentModificationException:出現 fail-fast 問題的時候就會丟擲這個異常。

可能問題描述得有點抽象,我舉個例子:假設有個 ArrayList 集合A,A裡面包含10個元素,分別是0~9。假設執行緒a在獲取第5個元素的過程中,執行緒b操作A刪除了第一個元素。那麼問題來了,此時a執行緒是獲取的到結果是5,但是我的本意應該是取到結果4,此時程式發生了錯誤,因此產生 fail-fast 問題,遂丟擲異常。

###解決方案

  1. 在遍歷過程中所有涉及到改變modCount值得地方全部加上synchronized。
  2. 用 CopyOnWriteArrayList,ConcurrentHashMap 替換 ArrayList, HashMap,它們的功能和名字一樣,在寫入時會建立一個 copy,然後在這個 copy 版本上進行修改操作,這樣就不會影響原來的迭代。不過壞處就是浪費記憶體。

Iterator 實現迭代功能

Iterator 的實現類一般以內部類的形式寫在集合類裡面。功能的實現是根據各種集合實現的特定實現,比如說 ArrayList 和 LinkedArrayList 的資料結構不一樣,所以 Iterator 實現也不一樣。

這裡以 ArrayList 的 Iterator 舉例子講一下 Iterator 的程式碼實現。
在看原始碼之前,我們先來回顧一下 Iterator 的使用。

不使用 Iterator 遍歷集合是這樣的:
for(int i=0; i<list.size();i++){  
        // ... 
    } 
//使用 Iterator 遍歷集合是這樣的:

Iterator iterator = list.iterator();
while (iterator.hasNext()){
    Object obj = iterator.next()
}複製程式碼

一般情況,如果只是遍歷獲取集合的所有元素,我選擇使用第一種方式,因為用 iterator 感覺好麻煩的樣子。但是肯定很多童鞋都犯過一個這樣的錯誤,我們在 for 迴圈裡面對集合進行了remove操作,但是最後的結果和我們期望的不一樣,這時候老手告訴你,集合不能這樣操作,如果你要remove,請用 iterator操作,那樣不會出問題,於是,我們默默的記下了這個結論。稍後,我們會在Iterator 的原始碼裡面找到原因。

private class Itr implements Iterator<E> {
        int cursor;//當前角標位置
        int lastRet;//用來標記當前需要刪除的元素角標
        int expectedModCount;//ArrayList元素個數

    private Itr() {
        this.lastRet = -1;//角標預設指向-1
        this.expectedModCount = ArrayList.this.modCount;
    }

    public E next() {
        this.checkForComodification();//檢查 fail-fast
        int var1 = this.cursor;
        if(var1 >= ArrayList.this.size) {
            throw new NoSuchElementException();//角標越界
        } else {
            Object[] var2 = ArrayList.this.elementData;//ArrayList的底層實現實際上就是一個陣列
            if(var1 >= var2.length) {//double check
                throw new ConcurrentModificationException();
            } else {
                this.cursor = var1 + 1;
                return var2[this.lastRet = var1];
            }
        }
    }

    public boolean hasNext() {
        //檢查是否大於陣列長度
        return this.cursor != ArrayList.this.size;
    }

    public void remove() {
        if(this.lastRet < 0) {
            throw new IllegalStateException();
        } else {
            this.checkForComodification();
            try {
                ArrayList.this.remove(this.lastRet);//呼叫ArrayList的remove
                this.cursor = this.lastRet;//cursor
                this.lastRet = -1;//避免同時呼叫兩次remove
                this.expectedModCount = ArrayList.this.modCount;//更新陣列長度
            } catch (IndexOutOfBoundsException var2) {
                throw new ConcurrentModificationException();
            }
        }
    }
}複製程式碼

注視我都寫在程式碼裡面了,其實ArrayList.Iterator 就是一個對陣列的遍歷,較之直接 for()迴圈ArrayList,優點是做了 fail-fast 檢查,並且增加了在遍歷過程中刪除的功能。

再來詳細講一下for()迴圈裡面不能用list.remove(i)的原因。因為在 for(int i=0; i<list.size();i++) 語句中,假設 list 有4個元素,假如果在 i = 0 的時候呼叫了list.remove(i),此時就出現了取值錯位並且漏值的情況。但是在Iterator 裡面,我們可以看到有一行這樣的程式碼 this.cursor = this.lastRet 改變了當前陣列的角標。

這裡分享一個使用for迴圈然後再在迴圈裡面刪除值並且不會出錯的辦法

for (int i = list.size()-1; i >=0; i--) {
    String s = list.get(i);
    if (s.equals("remove")) {
        list.remove(i);
    }
}複製程式碼

好了,很簡單的邏輯,Iterator 就分享到這裡吧,不同的集合裡面的 Iterator 的實現方式不一樣,但是邏輯都是一樣的,所以就不再贅述了。

古耐~

相關文章