Java集合學習記錄——Iterator

SachinLea發表於2019-02-01

Iterator學習記錄

1. Java集合類圖

Java集合學習記錄——Iterator
圖片來源:菜鳥教程
  上圖中,上圖中實線邊框的是實現類,折線邊框的是抽象類,點線邊框的是介面。
  從圖中我們可以看出,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之間的位置,同時返回遊標前的元素。(該遊標只能單向移動)

iterator遊標簡圖

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();
  }
複製程式碼

  根據以上程式碼,首先需要判斷expectedModCountmodCount是否相等,如果不相等會丟擲異常,也就是如果獲取了迭代器之後,又修改了集合,那麼使用迭代器的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,然後,還是判斷expectedModCountmodCount是否相等,然後呼叫ArrayListremove(int index)方法,但是,刪除之後,modCount會被修改,因此需要讓expectedModCount 和它再次相等,防止後邊使用迭代器的相關方法報錯;

  每次移除的是遊標上一次所在位置索引的元素,同時判斷lastRet不能小於0,而移除元素之後和最開始獲取迭代器時,lastRet都是-1;也就是說,獲取迭代器之後,不能立馬移除元素,也不能在使用remove()方法後,不使用next()方法移動遊標;

  同時移除之後,集合ArrayList中的所有元素會往前移一位,如果遊標的位置不修改,就會導致發生遺漏元素;因此,遊標需要重新指向之前所在位置。

  關於刪除方法,還有一個常見問題,就是在迴圈中移除元素,這個問題,稍後在ArrayList的學習整理中記錄。

相關文章