Iterator學習記錄
1. Java集合類圖
圖片來源:菜鳥教程上圖中,上圖中實線邊框的是實現類,折線邊框的是抽象類,點線邊框的是介面。
從圖中我們可以看出,Java集合主要包含了Collection(主要儲存元素集合)和Map(主要儲存鍵值對集合)兩種型別。
2. Iterator
2.1 Iterator介紹
從上圖可以看出,Iterator是整個集合框架的起始點,因此,先學習這個Iterator。
Iterator是Java集合中的迭代器,也是設計模式中的迭代器模式的實現,在Java遍歷一個集合,除了使用for迴圈和增強for迴圈外,還可以使用迭代器來遍歷集合(forEach迴圈實際也是使用了迭代器來遍歷)。
-
Iterator 簡單使用
以
ArrayList
為例,展示Iterator的基本使用
private static void test() {
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add("1");
arrayList.add("2");
arrayList.add("3");
Iterator iterator = arrayList.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}
複製程式碼
可以看到,Iterator使用中,首先呼叫iterator()
方法,獲取迭代器,然後,迴圈中使用hasNext()
方法判斷是否含有下一個元素,使用next()
方法獲取當前的元素。
2.2 Iterator原始碼
2.2.1 主要方法
Iterator中的主要方法有:
public interface Iterator<E> {
// 判斷是否含有下一個元素
boolean hasNext();
// 獲取下一個元素
E next();
// 移除元素,主要靠實現類實現
default void remove() {
throw new UnsupportedOperationException("remove");
}
// Java8 新增方法,方便直接遍歷
default void forEachRemaining(Consumer<? super E> action) {
Objects.requireNonNull(action);
while (hasNext())
action.accept(next());
}
}
複製程式碼
在上邊的使用中主要使用前兩個方法,除了這兩個方法外,Iterator中還有一個remove()
方法,主要依賴實現類實現;另外還有一個Java8新增的方法forEachRemaining()
可以直接使用該方法遍歷集合,可以看到該方法的預設實現和上邊例子中的使用相同。
forEachRemaining
的使用:
private static void test() {
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add("1");
arrayList.add("2");
arrayList.add("3");
Iterator iterator = arrayList.iterator();
iterator.forEachRemaining(a -> {
System.out.println(a);
});
}
複製程式碼
2.2.2 Iterator原理理解
Iterator迭代器的基本模型,可以理解為一個介於結合元素的中間位置的遊標。如下圖所示,呼叫hasNext()
方法,會檢視遊標後邊是否有資料,如果有就返回true,呼叫next()
方法,遊標往後移一位,到1和2之間的位置,同時返回遊標前的元素。(該遊標只能單向移動)
2.2.3 Iterator原始碼實現
Iterator只是一個介面,具體方法的實現,還是在具體的集合類中,我們以ArrayList
為例,檢視其具體實現,ArrayList
中使用了一個內部類Itr
實現了Iterator介面。
- 成員變數
private class Itr implements Iterator<E> {
int cursor; // 遊標位置索引,預設為0
int lastRet = -1; // 遊標上一次所在位置
// modCount:AbstractList中的成員變數,集合修改的次數;
// expectedModCount: 集合期望修改的次數,預設和modCount相等
int expectedModCount = modCount;
}
複製程式碼
-
hasNext()
方法:在上邊我們知道該方法,主要判斷集合中是否還有元素,也就是判斷遊標後邊是否還有元素,而根據成員變數中的幾個元素,猜測可以使用對應的索引來判斷,也就是
cursor
是否小於集合的長度,具體實現:
public boolean hasNext() {
return cursor != size;
}
複製程式碼
-
next()
方法:在檢視程式碼之前,猜測其實現方式:首先會將遊標往後移,也就是
cursor++
, 然後,返回遊標前的元素,在ArrayList
中就是遊標未修改之前的索引位置元素(LinkedList
中沒有實現Iterator介面)。
public E next() {
// 驗證 expectedModCount 是否和 modCount相等
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;
// 返回之前索引位置的元素,並且修改lastRet的值
return (E) elementData[lastRet = i];
}
/**
* 驗證 expectedModCount 是否和 modCount相等;
* modCount在修改集合(新增元素,刪除元素時會修改)
*/
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
複製程式碼
根據以上程式碼,首先需要判斷expectedModCount
和modCount
是否相等,如果不相等會丟擲異常,也就是如果獲取了迭代器之後,又修改了集合,那麼使用迭代器的next()
方法會報錯;然後,就是常規操作,遊標後移,返回之前索引的元素,為lastRet
賦值;
下邊驗證,獲取迭代器之後,修改集合:
private static void test() {
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add("1");
arrayList.add("2");
arrayList.add("3");
Iterator iterator = arrayList.iterator();
arrayList.add("4");
iterator.forEachRemaining(a -> {
System.out.println(a);
});
}
複製程式碼
執行之後,可以看到果然丟擲了異常,因此,在使用迭代器時需要注意,不能在獲取了迭代器之後仍然修改集合(新增、移除集合中的元素)。
Exception in thread "main" java.util.ConcurrentModificationException...
複製程式碼
remove()
方法:
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
// 判斷 expectedModCount 和 modCount是否相等
checkForComodification();
try {
// 呼叫ArrayList中的remove方法,刪除遊標之前位置索引的元素,
// 也就是當前遊標的前一個元素。
ArrayList.this.remove(lastRet);
// 遊標移回上一次所在位置
cursor = lastRet;
// 遊標上一次位置,置為 -1
lastRet = -1;
// 修改 expectedModCount 值,是它和modCount相等
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
複製程式碼
根據上邊的程式碼,首先,判斷lastRet
是否小於0,然後,還是判斷expectedModCount
和modCount
是否相等,然後呼叫ArrayList
的remove(int index)
方法,但是,刪除之後,modCount
會被修改,因此需要讓expectedModCount
和它再次相等,防止後邊使用迭代器的相關方法報錯;
每次移除的是遊標上一次所在位置索引的元素,同時判斷lastRet
不能小於0,而移除元素之後和最開始獲取迭代器時,lastRet
都是-1;也就是說,獲取迭代器之後,不能立馬移除元素,也不能在使用remove()
方法後,不使用next()
方法移動遊標;
同時移除之後,集合ArrayList
中的所有元素會往前移一位,如果遊標的位置不修改,就會導致發生遺漏元素;因此,遊標需要重新指向之前所在位置。
關於刪除方法,還有一個常見問題,就是在迴圈中移除元素,這個問題,稍後在ArrayList
的學習整理中記錄。