Java——深入瞭解Java中的迭代器
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~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~pia!
相關文章
- 深入瞭解 Java 的 volatile 關鍵字Java
- java基礎之:迭代器詳解Java
- 深入瞭解Java JIT編譯器:原理與效能最佳化Java編譯
- (深入理解 Java虛擬機器)一篇文章帶你深入瞭解Java 虛擬機器類載入器Java虛擬機
- 【java】【集合】迭代器IteratorJava
- 淺談Java迭代器Java
- 深入瞭解 Java 方法和引數的使用方法Java
- 解鎖Java面試中的鎖:深入瞭解不同型別的鎖和它們的用途Java面試型別
- Java中反射的概述及瞭解ClassLoaderJava反射
- Java基礎Iterator迭代器Java
- 你瞭解 Java 的類載入器嗎?Java
- Java中Iterator迭代器的next()方法的新手易錯點。Java
- 深入瞭解JavaScript中的物件JavaScript物件
- 兩分鐘瞭解Java中volatile!Java
- Java 集合(2)之 Iterator 迭代器Java
- 深入瞭解機器學習機器學習
- Java加密解密瞭解Java加密解密
- Java程式設計:一步步教你如何深入瞭解神秘的Java反射機制Java程式設計反射
- 深入瞭解這些Java框架,看看哪個更適合你?Java框架
- 深入理解Java中的AQSJavaAQS
- 深入解析Java中的泛型Java泛型
- 深入理解Java中的鎖Java
- 深入理解 Java 中的 LambdaJava
- 深入瞭解 TiDB SQL 優化器TiDBSQL優化
- 你瞭解 Java 的逃逸分析嗎?Java
- 你瞭解Java反射嗎?Java反射
- Java集合類初步瞭解Java
- 更深入的理解Python中的迭代Python
- 更深入的理解 Python 中的迭代Python
- 深入理解 Java 註解Java
- 深入瞭解jquery中的ajax方法引數jQuery
- 深入瞭解MySQL中的自增主鍵MySql
- 深入瞭解Azure 機器學習的工作原理機器學習
- 深入瞭解JVM虛擬機器8:Java的編譯期最佳化與執行期最佳化JVM虛擬機Java編譯
- 深入理解Java中的逃逸分析Java
- 深入理解Java中的鎖(二)Java
- 深入理解Java中的鎖(一)Java
- 深入理解Java中的Garbage CollectionJava