1. 程式人生 > >iterator介面原始碼分析(ArrayList中的實現)

iterator介面原始碼分析(ArrayList中的實現)

iterator是一個迭代器介面,它裡面主要有:

boolean hasNext();
E next();

這兩個方法,第一個方法表示迭代器含有更多元素則返回true;否則返回false;

第二個方法是返回迭代器的下一個元素;

其中還有兩個實現方法:

    default void remove() {
        throw new UnsupportedOperationException("remove");
    }
    default void forEachRemaining(Consumer<? super E> action) {
        Objects.requireNonNull(action);
        while (hasNext())
            action.accept(next());
    }

接下來我們來看看在不同集合類中是如何去實現迭代器的:

1.ArrayList中的實現方式:

    public Iterator<E> iterator() {
        return new Itr();
    }

我們可以看到在這裡使用了內部類:

 private class Itr implements Iterator<E> {
        int cursor;       // 返回下一個元素的索引
        int lastRet = -1; // 返回最後一個元素的索引,如果沒有就這樣
        int expectedModCount = modCount;

        Itr() {}

接下來我們看一下hasNext():

        public boolean hasNext() {
            return cursor != size;
        }
在這裡使用了cursor與size比較返回的boolean值;
        @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];
        }

這裡呼叫的方法:

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

這個方法是什麼作用呢?就是Fail-Fast機制,就是expectedModCount預期修改的次數,modCount就是實際修改的次數;

在這迭代器中,首先定義了一個expectedModCount=modCount預設值是0;這樣通過上面的checkForComodification()方法就可以判斷list集合在迭代過程中是否被其他執行緒修改過,這裡的修改就是集合的增刪改查;因為這些操作都能引起modCount數值加1的操作,這樣就使得迭代失敗了。

            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];

這一段需要說一下:

第一:為什麼我們要建立一個elementData陣列,而不是直接ArrayList.this.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();
            }
        }

迭代器的移除方法,這裡先不說實現,先問一個問題,既然迭代器也有移除,list本身也有移除,那麼他們之間有什麼不同呢?

那我們接下來來看一下ArrayList下的remove方法:

    public E remove(int index) {
        rangeCheck(index);

        modCount++;
        E oldValue = elementData(index);

        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
    }

這裡有個rangeCheck(index)方法,它是幹什麼的?

    private void rangeCheck(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

我們能夠看到,它是當前集合座標大於等於集合size的時候丟擲下標越界異常;接著我們往下看,這裡有個我們熟悉的變數 modCount++;在迭代器中我們有與之判斷的變數:

        final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
就是這個方法,如果使用ArrayList的remove方法移除的話那麼 modCount就會加1,這樣迭代器就會報錯,所以說在使用迭代器進行遍歷的時候是不能使用移除,新增等功能的;

E oldValue = elementData(index);

是要被移除的元素;

int numMoved = size - index - 1;

這個是計算被移除的元素座標之後的長度,為什麼要減1呢?由於座標是從0開始的。

        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null;

當移除的不是最後一個元素的時候,就進行陣列複製,這裡說一下詳細的引數含義:

elementData:是原陣列;

index+1:原陣列複製的起始位置;

elementData:目標陣列;

index;目的陣列放置的起始位置;

numMoved:複製的長度;

執行完之後將原陣列置位null;便於jvm回收;

接下來我們看下迭代器實現的移除方法,看看有什麼不一樣的。

首先不同的是ArrayList裡面的remove需要傳參:1)一種是傳指標2)一種是傳元素

然後我們看他們的內部實現:

try {
                ArrayList.this.remove(lastRet);
                cursor = lastRet;
                lastRet = -1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
可以看到,迭代器的remove首先呼叫了ArrayList的remove方法,也就是咱們上面說的那些,

那這裡喲偶什麼不同呢?

1)這裡沒有進行modCount++;也不能這麼說,其實還是有的,那就是呼叫ArrayList的remove時,在它裡面進行的modCount++;

那既然這樣,那迭代的時候移除元素,不一樣會會使得checkForComodification();方法丟擲異常嗎?

那我們來看第二個不同:

2)它這裡處理的很巧妙,就是最後再重新給expectedModCount賦值,這樣就能一直保持一樣了,這一點ArrayList中的remove方法卻沒有這個;

                cursor = lastRet;
                lastRet = -1;
                expectedModCount = modCount;

然後我們還看到一個問題,那就是另兩個賦值是幹嘛用的?

這就要結合迭代器的next來看了,也就是說:只有當迭代器限制性next之後,才能remove,否則remove會跑出異常,why?

我們來看一下開頭的判斷:

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

lastRet判斷小於0就要丟擲異常,而我們預設的值是-1;那麼好吧肯定得獲取到元素的索引吧,而且你也能看到在上面remove方法中lastRet又重新賦值了-1;這樣就可以看出來如果不去執行其他的操作,而直接使用remove是不可能的。

我們再來看一下next方法,它裡面有一句這樣的賦值:

           return (E) elementData[lastRet = i];

i是什麼?就是cursor:下一個元素的索引,而在這裡i卻是當前的索引,因為在cursor+1之前,就已經賦值給i了,所以這裡是當前的索引,也就是0;

到這裡你是不是已經看出來上面的問題了,只有每次呼叫next方法獲取當前的索引,你才能夠將其移除。

還有一個forEachRemaining方法,這個就不說了,這是jdk1.8加入的一種Lamdba表示式。

到這裡我們可以來做幾個小例子來鞏固一下:

1)看一下下面的程式會不會執行結果是什麼?

	public static void main(String[] args) {
		ArrayList<String> a=new ArrayList<String>();
		for(int j=0;j<5;j++){
			a.add("a");
		}
		Iterator<String> iterator = a.iterator();
		while(iterator.hasNext()){
			iterator.remove();
		}
		System.out.println(a.size());
	}
這裡執行後會跑出異常:


就是這裡,我們沒有獲取當前的索引;

2)這個會怎麼樣

	public static void main(String[] args) {
		ArrayList<String> a=new ArrayList<String>();
		for(int j=0;j<5;j++){
			a.add("a");
		}
		for(int i=0;i<a.size();i++){
			if("a".equals(a.get(i))){
				a.remove(i);
			}
		}
		System.out.println(a.size());
	}

這個會正常執行的,但是結果卻是存在問題的,這是因為你每次遍歷的時候i都加了1,但是你移除之後,原來的元素的索引都產生了變化,即減了1,這樣下一次再移除的時候就會從索引1開始,這樣變成0的索引就無法刪除了,這裡考察了那個問題就是remove中的複製:

            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);

3)那增強for迴圈呢?

	public static void main(String[] args) {
		ArrayList<String> a=new ArrayList<String>();
		for(int j=0;j<5;j++){
			a.add("a");
		}
		for(String b:a){
			if("a".equals(b)){
				a.remove(b);
			}
		}
		System.out.println(a.size());
	}

這個一樣是跑出異常的,

首先說一下,增強for迴圈其實就是迭代器的next方法,這個大哥斷點你就可以看到,那麼好了我們知道了它呼叫了迭代器的next方法,此方法裡面有個 checkForComodification();校驗,第一次是成功的進入了,然後呼叫ArrayList的remove方法,modCount++了,而ArrayList中的remove有沒有最後 expectedModCount = modCount;進行賦值,因此會跑出異常:

        final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
恩,這樣就可以了,那有沒有可以一邊迭代一邊修改的迭代器呢,有的,就是listIterator迭代器,看名字就知道它的作用是list集合,而不是適應所有的集合的。這個之後再說。