1.問題引入
今天好哥們突然給我發來一條QQ消息,問迭代器能不能修改集合,用過迭代器的小夥伴應該都知道這一點:迭代器是用來統一對於List和Set集合的遍歷方式的,至於能不能修改集合中的元素,講真的,我沒去思考過。不過當時爲了快點回復哥們(其實是爲了遮掩自己也不知道的慘狀),我就建議他去看下Iterator的接口定義,隨後悄咪咪的又學習了一波迭代器,於是便有了這篇博文。
2.迭代器回顧
2.1 迭代器是什麼
- 可以先看下圖,總覽一下整個集合框架。
-
我們知道不同集合的底層實現是不一樣的,比如ArrayList的底層是順序表,LinkedList的底層是鏈表,HashSet的底層是hash表,它們的底層實現從根本上決定了它們自身的特性,其中就包括遍歷的方式。具有索引的集合(比如List集合)可以採用for循環來遍歷,但是由於Set集合不包含索引,所以Set集合就不能使用for循環來遍歷。對不同集合遍歷的前提是必須瞭解各個集合的底層實現,這使得集合與其遍歷方式存在很強的耦合關係。
-
迭代器的存在就是爲了解決上述強耦合的關係,它總是用同一種邏輯來遍歷集合,使得用戶自身不需要了解集合的內部結構,所有的內部狀態都由Iterator來維護。也就是說,用戶不用直接和集合進行打交道,而是控制Iterator向它發送向前向後的指令,就可以遍歷集合。
-
我們看一下迭代器接口,可以看到迭代器中定義了四個方法:hasNext(),next(),remove(),forEachRemaining()。
2.2 集合框架是如何實現迭代器接口的
Iterator接口只是在頂層設計了規範,那麼這些規範是如何實現的呢?這裏以ArrayList爲例子,看看它是如何實現迭代器接口的。
-
ArrayList的繼承和實現的體系結構
-
從上圖中我們並沒有看到terator接口,但是看到了Iterable接口,那麼這個Iterable接口又和Iterator接口有什麼聯繫呢?到這裏我的猜想是Iterable接口內部封裝了Iterator接口,之所以這樣猜想,是因爲沒有更加合理的地方來封裝Iterator藉口了。
-
如下圖所示,我看了下Iterable的源碼,發現其內部的確封裝了Iterable接口。OK,我們離答案又近了一步,接下繼續抽絲剝繭。
-
接下來的問題就是,哪個類又實現了Iterable接口?下面查看Collection接口、AbstractCollection接口、AbstractList接口,看看能不能找到Iterator的蹤跡。
-
Collection接口繼承了Iterable接口,沒有對Iterator接口的實現或繼承
-
List接口繼承了Collection接口,同樣沒有對Iterator接口的實現或繼承
-
AbstractCollection是一個抽象類,它繼承了Collection接口,在它的一些方法裏我看到了Iterator接口的方法hasNext()和next(),這說明我們離答案又進了一步。
-
分析到這裏我遇到了一個問題:AbstractCollection裏只是定義了一個返回值爲Iterator的抽象方法,爲什麼AbstractCollection裏面的方法就可以調用Iterator接口的方法了?
我的理解是這裏的抽象方法雖然沒有實現,但是在抽象類中依然可以調用這個抽象方法,把視角放在最終的實現類一切就都合理了。那麼這個抽象方法在最終的實現類中是如何實現的呢?繼續往下看。 -
再看一眼繼承和實現的結構,我們下一步應該探究一下AbstractList了,那就一起來看一看吧。
-
AbstractList類繼承了AbstractCollection抽象類,實現了List接口,它內部定義了5個成員類,具體啥作用後面再說。
AbstractList類實現了返回值爲Iterator的抽象方法iterator(),它return了一個Itr對象,那麼這個Itr對象又是什麼呢,我們跳轉到Itr處看它的具體定義
可以看到Itr實現了Iterator接口,到這裏終於是看到Iterator接口本尊了,答案也就快水落石出了
Ltr的源碼如下(截圖的話篇幅不夠,直接上源碼,中文註釋是自己家的):
private class Itr implements Iterator<E> {
/**
* Index of element to be returned by subsequent call to next.
*/
int cursor = 0;
/**
* Index of element returned by most recent call to next or
* previous. Reset to -1 if this element is deleted by a call
* to remove.
*/
int lastRet = -1;
/**
* The modCount value that the iterator believes that the backing
* List should have. If this expectation is violated, the iterator
* has detected concurrent modification.
*/
int expectedModCount = modCount;
//實現hasNext()方法
public boolean hasNext() {
return cursor != size();
}
//實現next()方法
public E next() {
checkForComodification();
try {
int i = cursor;
E next = get(i);
lastRet = i;
cursor = i + 1;
return next;
} catch (IndexOutOfBoundsException e) {
checkForComodification();
throw new NoSuchElementException();
}
}
//實現remove()方法
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
AbstractList.this.remove(lastRet);
if (lastRet < cursor)
cursor--;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException e) {
throw new ConcurrentModificationException();
}
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
- 到這裏我們已經基本上解決了集合框架是如何實現Iterator接口的問題,但還有一個問題沒有解決,就是到現在爲止,我們知道了關鍵在於下圖這個方法,實際上我們只需要用到Iterator,但是爲什麼要用Iterable接口?
先不急着說爲什麼,可以先看一下LinkedList中是如何實現的
private class ListItr extends Itr implements ListIterator<E> {
ListItr(int index) {
cursor = index;
}
public boolean hasPrevious() {
return cursor != 0;
}
public E previous() {
checkForComodification();
try {
int i = cursor - 1;
E previous = get(i);
lastRet = cursor = i;
return previous;
} catch (IndexOutOfBoundsException e) {
checkForComodification();
throw new NoSuchElementException();
}
}
public int nextIndex() {
return cursor;
}
public int previousIndex() {
return cursor-1;
}
public void set(E e) {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
AbstractList.this.set(lastRet, e);
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
public void add(E e) {
checkForComodification();
try {
int i = cursor;
AbstractList.this.add(i, e);
lastRet = -1;
cursor = i + 1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
}
再來看一下HashSet是如何實現的
abstract class HashIterator {
Node<K,V> next; // next entry to return
Node<K,V> current; // current entry
int expectedModCount; // for fast-fail
int index; // current slot
HashIterator() {
expectedModCount = modCount;
Node<K,V>[] t = table;
current = next = null;
index = 0;
if (t != null && size > 0) { // advance to first entry
do {} while (index < t.length && (next = t[index++]) == null);
}
}
public final boolean hasNext() {
return next != null;
}
final Node<K,V> nextNode() {
Node<K,V>[] t;
Node<K,V> e = next;
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
if (e == null)
throw new NoSuchElementException();
if ((next = (current = e).next) == null && (t = table) != null) {
do {} while (index < t.length && (next = t[index++]) == null);
}
return e;
}
public final void remove() {
Node<K,V> p = current;
if (p == null)
throw new IllegalStateException();
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
current = null;
removeNode(p.hash, p.key, null, false, false);
expectedModCount = modCount;
}
}
通過上面的兩個例子可以知道不同的集合對迭代器的實現也有不同的方法,Iterable接口對Iterator進行了封裝,利用接口的多態將是實現的多樣性隱藏起來,實際操作時只需調用一個接口就行,這實質上就是設計模式種的工廠模式的體現。
- 到了這裏,對迭代器的底層源碼也有了一個大概的掌握,後期有機會再研究研究具體的細節。
3. 迭代器應用實踐
//一、List集合
List<String> list = new ArrayList<>();
list.add("henrly");
list.add("nancy");
list.add("lucy");
list.add("jeacy");
//遍歷List集合
//1.使用for循環
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
//2.使用迭代器
Iterator<String> it = list.iterator();
while (it.hasNext()) {
System.out.println(it.next());
}
//3.使用增強for循環
for (String l : list) {
System.out.println(l);
}
//二、Set集合
Set<Integer> set = new TreeSet<>();
set.add(111);
set.add(222);
set.add(333);
set.add(444);
//遍歷Set集合
//Set集合無索引,所以無法使用for循環遍歷
//1.使用迭代器
Iterator<Integer> it = set.iterator();
while (it.hasNext()) {
System.out.println(it.next());
}
//2.使用增強for循環
for (Integer i : set) {
System.out.println(i);
}
4. 能不能使用迭代器修改集合中的元素
- Iterator是工作在一個獨立的線程中,並且擁有一個 mutex 鎖。 Iterator被創建之後會建立一個指向原來對象的單鏈索引表,當原來的對象數量發生變化時,這個索引表的內容不會同步改變,所以當索引指針往後移動的時候就找不到要迭代的對象,所以按照 fail-fast 原則 Iterator 會馬上拋出java.util.ConcurrentModificationException異常。所以 Iterator 在工作的時候是不允許被迭代的對象被改變的。
- 但你可以使用 Iterator 本身的方法 remove() 來刪除對象,Iterator.remove() 方法會在刪除當前迭代對象的同時維護索引的一致性。
總結
通過對源碼的解讀,我對迭代器有了深入的瞭解。Java的源碼不是憑空而出的,是這個行業的前輩們的設計並實現的,看了這些大佬們的代碼,感覺自己現在的技術是真的菜,還有很大的提升空間,以後一定要多讀源碼,多動手。