Java——深入瞭解Java中的迭代器

一隻野生飯卡丘發表於2018-12-22

Java集合框架的集合類,我們有時候稱之為容器。容器的種類有很多種,比如ArrayList、LinkedList、HashSet...,每種容器都有自己的特點,ArrayList底層維護的是一個陣列;LinkedList是連結串列結構的;HashSet依賴的是雜湊表,每種容器都有自己特有的資料結構。

因為容器的內部結構不同,很多時候可能不知道該怎樣去遍歷一個容器中的元素。所以為了使對容器內元素的操作更為簡單,Java引入了迭代器模式! 

那什麼是迭代器呢?

迭代器(Iterator)是一個物件,它的工作就是遍歷並選擇序列中的物件,它提供了一種訪問容器(container)物件中的各個元素,而又不必暴露該物件內部細節的方法。

 

為什麼說迭代器可以使對容器內元素操作更為簡單呢?上文的描述說到“不必暴露該物件的內部細節”,這句話可辦了許多開發人員的大忙啦。因為它,開發人員不再需要了解容器底層的結構就可以實現對容器的遍歷。而由於建立迭代器的代價非常小,因此迭代器也通常被稱為輕量級的容器。

不過看到這裡,大家也許心裡可能會有一個疑問——“迭代器憑什麼能不暴露物件內部細節呢?”

這就到了迭代器最核心的思想——迭代器把訪問邏輯從不同型別的集合類中抽取出來,從而避免向外部暴露集合的內部結構。

為什麼說它把訪問邏輯從不同型別的集合類中抽取出來,就能省很多事情呢?

我們先來看看陣列和ArrayList的處理是怎麼樣的。

對於陣列來說,我們大多通過下標來訪問內部元素

 int array[] = new int[10];    
 for (int i = 0; i < array.length; i++) {
     System.out.println(array[i]);
 }

而對於ArrayList的處理如下

List<String> list = new ArrayList<String>();
    for(int i = 0 ; i < list.size() ;  i++){
       String string = list.get(i);
}

對於這兩種方式,我們總是都知道它的內部結構,訪問程式碼和集合本身是緊密耦合的,因此無法將訪問邏輯從集合類和客戶端程式碼中分離出來。而由於不同的集合會對應不同的遍歷方法,所以客戶端程式碼無法複用。在實際應用中如何將上面兩個集合整合是相當麻煩的。

而對於我們們今天的主角Iterator來說,它總是用同一種邏輯來遍歷集合,使得客戶端自身不需要維護集合的內部結構,所有的內部狀態都由Iterator來維護。也就是說,客戶端不用直接和集合進行打交道,而是控制Iterator向它傳送向前向後的指令,就可以遍歷集合。

下面我們們就來深入瞭解一下Iterator。

1.java.util.Iterator

在Java中Iterator為一個介面,它只提供了迭代的基本規則。在JDK中它是這樣定義的:對Collection進行迭代的迭代器。迭代器取代了Java Collection Framework中的Enumeration。迭代器與列舉有兩點不同:

  • 迭代器在迭代期間可以從集合中移除元素。
  • 方法名得到了改進,Enumeration的方法名稱都比較長。

其介面定義如下:

package java.util;
public interface Iterator<E> {
    boolean hasNext();//判斷是否存在下一個物件元素
    E next();//獲取下一個元素
    void remove();//移除元素
}

2.Iteratable介面 

Java中還提供了一個Iterable介面,Iterable介面實現後的功能是‘返回’一個迭代器,我們常用的實現了該介面的子介面有:Collection<E>、List<E>、Set<E>等。該介面的iterator()方法返回一個標準的Iterator實現。實現Iterable介面允許物件成為Foreach語句的目標,就可以通過foreach語句來遍歷你的底層序列。

Iterable介面包含一個能產生Iterator物件的方法,並且Iterable被foreach用來在序列中移動。因此如果建立了實現Iterable介面的類,都可以將它用於foreach中。

Iterable介面的具體實現:

Package java.lang;

import java.util.Iterator;
public interface Iterable<T> {
    Iterator<T> iterator();
}

 3.迭代器的使用

迭代器的使用主要有以下三個方面的注意事項:

  • 使用容器的iterator()方法返回一個Iterator物件,然後通過Iterator的next()方法返回第一個元素。
  • 使用Iterator的hasNext()方法判斷容器中是否還有元素,如果有,可以使用next()方法獲取下一個元素。
  • 可以通過remove()方法刪除迭代器返回的元素。
     

Iterator的使用示例如下:

package Test;

import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

public class IteratorTest {
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		List<String>ll=new LinkedList<String>();
		ll.add("first");
		ll.add("second");
		ll.add("third");
		ll.add("fourth");
		for(Iterator<String>iter=ll.iterator();iter.hasNext();)
		{
			String str=(String)iter.next();
			System.out.println(str);
		}
	}
}

程式的執行結果如下:

當然,我們也可以將for迴圈改為更加簡潔明瞭的for-each迴圈,如下圖:

4.ConcurrentModificationException異常

上面說到,如果用迭代器的話,可以不需要了解內部結構,似乎很好用的樣子。但是,總有奇思異想的小夥子:在使用Iterator比遍歷容器的同時又對容器進行增加或者刪除操作的話,會怎麼樣呢?

我們們寫一個程式來看看。

package Test;

import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

public class IteratorTest1 {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		List<String>ll=new LinkedList<String>();
		ll.add("first");
		ll.add("second");
		ll.add("third");
		ll.add("fourth");
		for(Iterator<String>iter=ll.iterator();iter.hasNext();)
		{
			String str=(String)iter.next();
			System.out.println(str);
			if(str.equals("second"))
			{
				ll.add("five");
			}
		}
	}
}

大家覺得輸出結果會是什麼呢?且看下圖。

 執行的時候報錯了。這是為什麼呢?

有道是“一旦不理解就看原始碼”,所以我們們來看看Iterator原始碼是如何寫的,下邊是原始碼:

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();
        }
    }

通過檢視原始碼,我們可以發現丟擲異常的是checkForComodification()方法。在ArrayList中modCount是當前集合的版本號,每次修改(增、刪)集合都會加1;expectedModCount是當前迭代器的版本號,在迭代器例項化時初始化為modCount。我們看到在checkForComodification()方法中就是在驗證modCount的值和expectedModCount的值是否相等,所以當你在呼叫了ArrayList.add()或者ArrayList.remove()時,只更新了modCount的狀態而迭代器中的expectedModCount未同步,因此才會導致再次呼叫Iterator.next()方法時丟擲異常。但是為什麼使用Iterator.remove()就沒有問題呢?通過原始碼的第32行發現,在Iterator的remove()中同步了expectedModCount的值,所以當你下次再呼叫next()的時候,檢查不會丟擲異常。

使用該機制的主要目的是為了實現ArrayList中的快速失敗機制(fail-fast),在Java集合中較大一部分集合是存在快速失敗機制的。

快速失敗機制產生的條件:當多個執行緒對Collection進行操作時,若其中某一個執行緒通過Iterator遍歷集合時,該集合的內容被其他執行緒所改變,則會丟擲ConcurrentModificationException異常。

而上面我們說了實現了Iterable介面的類就可以通過Foreach遍歷,那是因為foreach要依賴於Iterable介面返回的Iterator物件,所以從本質上來講,Foreach其實就是在使用迭代器,在使用foreach遍歷時對集合的結構進行修改,和在使用Iterator遍歷時對集合結構進行修改本質上是一樣的。所以同樣的也會丟擲異常,執行快速失敗機制。

注:foreach是JDK1.5新增加的一個迴圈結構,foreach的出現是為了簡化我們遍歷集合的行為。

所以要保證在使用Iterator遍歷集合的時候不出錯誤,就應該保證在遍歷集合的過程中不會對集合產生結構上的修改。

如麼如何解決這種錯誤呢?解決方法如下:

在遍歷的過程中把需要刪除的物件儲存到一個集合中,等遍歷結束之後再呼叫removeAll()方法來刪除,或者使用iter.remove()方法。

 以上主要介紹了單執行緒的解決方法,那麼多執行緒訪問容器的過程中丟擲ConcurrentModificationException異常的話又該咋辦呢?

  • 在JDK1.5版本中引入了執行緒安全的容器,比如ConcurrentHashMap和CopyOnWriteArrayList等,可以使用這些執行緒安全的容器來代替非執行緒安全的容器。
  • 在使用迭代器遍歷容器的時候對容器的操作放到synchronized程式碼塊中,但是當引用程式併發成都比較高的時候,這會嚴重影響程式的效能。

5. for迴圈與迭代器的比較

每個方法都有不同的語境,因此它們沒有絕對的好也沒有絕對的壞,因此在效率上各有各的優勢:

  • ArrayList對隨機訪問比較快,而for迴圈中使用的get()方法,採用的即是隨機訪問的方法,因此在ArrayList裡for迴圈快。
  • LinkedList則是順序訪問比較快,Iterator中的next()方法採用的是順序訪問方法,因此在LinkedList裡使用Iterator較快。

不過總的來說,這兩種東西的好壞主要還是要依據集合的資料結構不同的判斷。

引申:Iterator與ListIterator有什麼區別?

Iterator只能正向遍歷集合,適用於獲取移除元素。ListIerator繼承自Iterator,專門針對List,可以從兩個方向遍歷List,同時支援元素的修改。

 

好啦,以上就是關於迭代器的相關知識總結啦,如果大家有什麼不明白的地方或者發現文中有描述不好的地方,歡迎大家留言評論,我們一起學習呀。

 

Biu~~~~~~~~~~~~~~~~~~~~宫å´éªé¾ç«è¡¨æå|é¾ç«gifå¾è¡¨æåä¸è½½å¾ç~~~~~~~~~~~~~~~~~~~~~~pia!

相關文章