1. 程式人生 > >通過ArrayList原始碼深入理解java中Iterator迭代器的實現原理

通過ArrayList原始碼深入理解java中Iterator迭代器的實現原理

注意:本文將著重從原始碼的角度對Iterator的實現進行講解,不討論List與Iterator介面的具體使用方法。不過看懂原始碼後,使用也就不是什麼問題了。

java中各種實現Iterator的類所具體使用的實現方法各不相同,但是都大同小異。因此本文將只通過ArrayList類原始碼進行分析。所以最好對ArrayList的原始碼有一定了解,或者至少具備相關的演算法知識。

首先貼出ArrayList類中與Iterator有關的程式碼。(不同版本jdk可能有微小差別)

    /**
     * Returns an iterator over the elements in this list in proper sequence.
     *
     * <p>The returned iterator is <a href="#fail-fast"><i>fail-fast</i></a>.
     *
     * @return
an iterator over the elements in this list in proper sequence */
public Iterator<E> iterator() { return new Itr(); } /** * An optimized version of AbstractList.Itr */ private class Itr implements Iterator<E> { int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such int expectedModCount = modCount; public boolean hasNext() { return cursor != size; } @SuppressWarnings("unchecked") public E next() { checkForComodification(); int
i = cursor; if (i >= size) throw new NoSuchElementException(); Object[] elementData = ArrayList.this.elementData; if (i >= elementData.length) throw new ConcurrentModificationException(); cursor = i + 1; return (E) elementData[lastRet = i]; } public void remove() { if (lastRet < 0) throw new IllegalStateException(); checkForComodification(); try { ArrayList.this.remove(lastRet); cursor = lastRet; lastRet = -1; expectedModCount = modCount; } catch (IndexOutOfBoundsException ex) { throw new ConcurrentModificationException(); } } final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); } }

很明顯,它是返回了一個內部類,通過這個內部類來實現。

由於需要考慮到各種各樣的情況,java中的原始碼總是會多出很多出於完善考慮的程式碼,非常影響理解功能實現的本質。這個時候如果想深入理解,最好的方法就是自己實現一個簡化版本,以此為基礎進行分析。

這是我寫的一個只實現了Iterable的簡化版ArrayList。

import java.util.Iterator;

public class Nayi224ArrayList implements java.lang.Iterable{

    //ArrayList實際上是一個維護Object[]的類,elementData才是本體。這裡先把資料寫死。
    public Object[] elementData = {1, 2, 3, 4};
    //平常所用的 .size() 其實就是return size,也先提前寫死。
    public int size = 4;

    public Iterator iterator(){
        return new IteratorImpl();
    };

    private class IteratorImpl implements java.util.Iterator{

        private int cursor; //遊標。int型成員變數預設初始值為0。

        public boolean hasNext() {
            return cursor != size;
        }

        public Object next() {
            return elementData[cursor++];
        }

        public void remove() {
            /*
             * ArrayList中remove操作的核心程式碼的改寫。
             * 完整版還包括下標越界校驗,modCount自增(fail-fast),末位置空(for gc)。均已省略。
             * 
             * 
             * System.arraycopy的用法可以直接檢視註釋獲得。
             * 
             * */
            System.arraycopy(elementData, cursor, elementData, cursor - 1, size - (cursor - 1) - 1);
        }

    }

}

所有程式碼都簡化成了一行,即使是新手也能看懂,這裡就不做多餘講解了。

在使用的時候,與原版完全一樣。(remove方法只實現了一半,與原版有較大差異)

public class BlogTest {

    public static void main(String[] args) {

        Nayi224ArrayList list = new Nayi224ArrayList();

        java.util.Iterator it = list.iterator();

        while (it.hasNext())
            System.out.println(it.next());
    }

}

這就是實現迭代功能的全部核心程式碼。

現在開始逐步理解原始碼中的其他部分。

首先是有名的fase-fail機制。它的作用是在迭代的時候,如果list發生了結構上的變化,將會丟擲異常。在ArrayList.Itr中主要通過這幾句話來實現。

        int expectedModCount = modCount;
        final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }

modCount是ArrayList所繼承的屬性,在對ArrayList進行結構上的修改,比如add,remove等方法時,會modCount++。

expectedModCount 是ArrayList的內部類Itr的屬性,會在初始化的時候賦值為外部類的modCount。
注意:雖然是int型別,但是這並不是值傳遞,而是一種特殊的引用傳遞。這與成員內部類的特性有關。通過外部類建立的內部類會保留外部類的引用。
int expectedModCount = modCount; 的另一種比較正規的寫法是
int expectedModCount = ArrayList.this.modCount;
如果用反編譯工具看的話可能會是這種東西this.this$0.modCount 。它是一種地址引用,這一點是理解快速失敗機制的關鍵。

checkForComodification()出現於Itr類中next和remove的開始部分。如果在操作一個Iterator的過程中對建立它的外部類進行了結構上的修改,將會丟擲ConcurrentModificationException異常。

我經常看到有人從併發的角度來講解Iterator的快速失敗機制。這很貼近現實,但是卻遠離了本質。這跟多執行緒有什麼關係呢,不過是迭代的時候ArrayList被修改了而已。(需要注意的是Itr的remove方法呼叫了外部類的remove方法,同樣會導致modCount++)。
如果你想搞炸一個程式,只需要這4行程式碼

        List arr = new ArrayList(Arrays.asList(1, 2, 3, 4));
        Iterator it =arr.iterator();
        arr.add(1);
        it.next();

初始化–add–迭代–boom!

相信很多人都在無數的資料中看到過類似的一句話。

迭代器的快速失敗行為無法得到保證,因為一般來說,不可能對是否出現不同步併發修改做出任何硬性保證。快速失敗迭代器會盡最大努力丟擲ConcurrentModificationException。

但是至少我還沒看到過有誰對這段話做過解釋。到底是在什麼情況下,它才會即使“盡最大努力”也無法丟擲異常。不過在看完原始碼後發現,這個問題好像確實簡單到不需要做特別說明的程度。

在remove方法中

        public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();
            //執行緒B進行add操作。
            try {
                ArrayList.this.remove(lastRet);
                cursor = lastRet;
                lastRet = -1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }

如果執行緒A在執行完checkForComodification(); 後,執行緒B立刻執行完了一次add,由於執行緒A重新對expectedModCount進行了賦值,這將導致無法對這次不同步的修改丟擲異常。這可能會導致意想不到的bug發生。

同樣的,在next方法中

        public E next() {
            checkForComodification();
            int i = cursor;
            if (i >= size)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];
        }

如果A執行緒在執行完第一行後發生了阻塞,而B執行緒在這時正好完成了一次add和一次remove。list的結構顯然改變了。A執行緒可能返回了B執行緒新新增的物件,卻無法丟擲異常。如果A執行緒繼續呼叫next方法,它將在下一次呼叫時丟擲異常,這與事實不符。

在Itr類中有這麼一個屬性
int lastRet = -1; // index of last element returned; -1 if no such
它主要出現在remove方法中。

        //初始值為-1
        int lastRet = -1; // index of last element returned; -1 if no such

        public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();

            try {
                ArrayList.this.remove(lastRet);
                cursor = lastRet;
                lastRet = -1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }

從程式碼上看,它的作用就兩個。一個是使剛初始化的迭代器直接呼叫remove時報錯,另一個是在正常呼叫remove後再掉一次remove引發報錯。個人覺得這應該是為了符合某種規範吧。

第一種報錯。

        List arr = new ArrayList(Arrays.asList(1, 2, 3, 4));
        Iterator it =arr.iterator();

        it.remove();

第二種報錯。

        List arr = new ArrayList(Arrays.asList(1, 2, 3, 4));
        Iterator it =arr.iterator();

        it.next();
        it.remove();
        it.remove();

重要的大概就這麼多了,剩下的基本都是些校驗下標越界或是一些更具體的實現。隨便看看也就懂了。