走進 JDK 之 ArrayList(二)

上篇文章 走進 JDK 之 ArrayList(一) 簡單分析了 ArrayList 的源碼,文末留下了一個問題,modCount 是幹啥用的?下面咱們經過一個小例子來引出今天的內容。java

public static void main(String[] args){
    List<String> list= new ArrayList<>();
    list.add("java");
    list.add("kotlin");
    list.add("dart");

    for (String s:list){
        if (s.equals("dart"))
            list.remove(s);
    }
}
複製代碼

大多數人應該都這麼幹過,而後獲得一個鮮紅的 ConcurrentModificationException,具體錯誤堆棧信息以下:安全

Exception in thread "main" java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
	at java.util.ArrayList$Itr.next(ArrayList.java:859)
	at collection.ArrayListTest.main(ArrayListTest.java:15)
複製代碼

報錯位置是 ArrayList 的內部類 Itr 中的 checkForComodification() 方法。至於如何調用到這個方法的,咱們首先得知道上面的代碼中發生了什麼。看字節碼的話太麻煩了又不容易理解,推薦一個反編譯神器 jad,javac 編譯獲得 class 文件以後執行以下命令:bash

./jad ArrayListTest.class
複製代碼

獲得 ArrayListTest.jad 文件,直接用文本編輯器打開便可:微信

public class ArrayListTest {

    public ArrayListTest() { }

    public static void main(String args[]) {
        ArrayList arraylist = new ArrayList();
        arraylist.add("java");
        arraylist.add("kotlin");
        arraylist.add("dart");
        Iterator iterator = arraylist.iterator(); // 1
        do {
            if(!iterator.hasNext()) // 2
                break;
            String s = (String)iterator.next(); // 3
            if(s.equals("dart"))
                arraylist.remove(s);
        } while(true);
    }
}
複製代碼

從反編譯獲得的代碼咱們能夠發現,加強型 for 循環只是一個語法糖而已,編譯器幫咱們進行了處理,實際上是調用了迭代器來進行循環。着重看一下上面標註的三句代碼,是整個迭代過程的核心。多線程

第一句,獲取 ArrayList 的迭代器。併發

public Iterator<E> iterator() {
    return new Itr();
}
複製代碼

AbstractList 中定義了一個迭代器 Itr,可是它的子類 ArrayList 並無直接使用父類的迭代器,而是本身定義了一個優化版本的 Itr。循環體中第二句代碼首先會判斷是否 hasNext(),存在的話調用 next 獲取元素,不存在的話跳出循環。加強型 for 循環的基本實現就是這樣的。hasNext()next() 方法源碼以下:框架

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;

    Itr() {}

    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];
    }

    ......
    // 省略其餘代碼
    }
複製代碼

cursur 表示當前遊標位置,hasNext() 方法就是根據 cursor 是否等於集合大小 size 判斷是否還有下一個元素。成員變量中有個 expectedModCount,定義以下:編輯器

int expectedModCount = modCount;
複製代碼

終於發現了 modCount 的蹤跡,它被賦值給了 expectedModCount 變量,字面意思就是 指望的修改次數。具體它有什麼用,接着看 next() 方法中的第一行代碼,調用了 checkForComodification() 方法,這是用來作併發檢測的:post

final void checkForComodification() {
    if (modCount != expectedModCount) // 在迭代的過程當中 modCount 發生了改變
        throw new ConcurrentModificationException();
}
複製代碼

異常就是這樣拋出來的,modCountexpectedModCount 不相等,即實際的修改次數與指望的修改次數不相等。expectedModCount 是在迭代器初始化的過程當中賦值的,其值等於 modCount。在迭代過程當中又不相等了,那就只多是在迭代過程當中修改了集合,形成了 modCount 變化。那麼,哪些操做會致使 modCount 發生變化呢?JDK 源碼註釋中作了如下說明(modCount 在 AbstractList 中聲明):測試

The number of times this list has been structurally modified. Structural modifications are those that change the size of the list, or otherwise perturb it in such a fashion that iterations in progress may yield incorrect results.

集合的結構修改次數。結構修改指的是集合大小的變化。因此只要是涉及到增長或者刪除元素的方法,都要改變 modCount。以 ArrayList 的 remove() 方法爲例:

public E remove(int index) {
    rangeCheck(index); // 邊界檢測

    modCount++;
    E oldValue = elementData(index);

    int numMoved = size - index - 1;
    if (numMoved > 0) // 移動 index 以後的全部元素
        System.arraycopy(elementData, index+1, elementData, index,
                        numMoved);
    elementData[--size] = null; // clear to let GC do its work

    return oldValue;
}
複製代碼

經過 modCount++ 使其自增 1。

因爲 ArrayList 並非線程安全的,一邊迭代一邊改變集合,的確可能致使多線程下代碼表現不一致。可能有人會有這樣的疑問,文章開頭的測試代碼並無涉及到併發操做啊,爲何仍是拋出了異常?這就是集合的 fail-fast(快速失敗) 機制。

fail-fast 錯誤機制並不保證錯誤必定會發生,可是當錯誤發生的時候必定能夠拋出異常。它無論你是否是真的併發操做,只要多是併發操做,就給你提早拋出異常。針對非線程安全的集合類,這是一種健壯的處理方式。可是你若是真的想在單線程中這樣操做應該怎麼辦?不要緊,讓 modCountexpectedModCount 相等就完事了,ArrayList 的迭代器爲咱們提供了這樣的 add()remove() 方法:

public void add(E e) {
    checkForComodification();

    try {
        int i = cursor;
        ArrayList.this.add(i, e); // add 以後要修改 modCount
        cursor = i + 1;
        lastRet = -1;
        expectedModCount = modCount;
    } catch (IndexOutOfBoundsException ex) {
        throw new ConcurrentModificationException();
    }
}

public void remove() {
    if (lastRet < 0)
        throw new IllegalStateException();
    checkForComodification();

    try {
        ArrayList.this.remove(lastRet); // remove 以後要修改 modCount
        cursor = lastRet;
        lastRet = -1;
        expectedModCount = modCount;
    } catch (IndexOutOfBoundsException ex) {
        throw new ConcurrentModificationException();
    }
}
複製代碼

上面的代碼實如今修改了集合結構以後都會給 expectedModCount 從新賦值,使其與 modCount 相等。修改一下文章開頭的測試代碼:

public static void main(String[] args){
    List<String> list= new ArrayList<>();
    list.add("java");
    list.add("kotlin");
    list.add("dart");

// for (String s:list){
// if (s.equals("dart"))
// list.remove(s);
// }

    Iterator<String> iterator=list.iterator();
    while (iterator.hasNext()){
        String s= iterator.next();
        if (s.equals("dart"))
            iterator.remove();
    }
}
複製代碼

這樣就不會再報錯了。

最後最後再給你出一道題,仔細看一下:

public static void main(String[] args) {
    List<String> list = new ArrayList<>();
    list.add("java");
    list.add("kotlin");
    list.add("dart");

    for (String s : list) {
        if (s.equals("kotlin"))
            list.remove(s);
    }
}
複製代碼

若是沒看出來和文章開頭那道題的區別,那就再翻上去仔細觀察一下。以前咱們要刪的是 dart,集合中的最後一個元素。如今要刪的是 kotlin,集合中的第二個元素。執行結果會怎麼樣?你要是精通腦筋急轉彎的話,確定能給出正確答案。沒錯,此次成功刪除了元素而且沒有任何異常。這是爲何呢?刪除 dart 就報異常,刪除 kotlin 就沒問題,這是歧視 dart 嗎。再把迭代器的代碼掏出來:

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];
}
複製代碼

集合中添加了 3 個元素,因此初始化迭代器以後,expectedModCount = modCount = 3cursor 此時爲 0 。先來分析文章開頭的代碼,刪除集合中最後一個元素的狀況:

  • 執行完第一次循環,cursor 爲 1,未產生刪除操做,modCount 爲 3,expectedModCount 爲 3,size 爲 3。cursor != sizehasNext() 判斷還有元素。
  • 執行完第二次循環,cursor 爲 2,仍未產生刪除操做,modCount 爲 3,expectedModCount 爲 3,size 爲 3。cursor != sizehasNext() 判斷還有元素。
  • 執行完第三次循環,cursor 爲 3,因爲產生刪除了操做,modCount 爲 4,expectedModCount 仍爲 3,size 變爲 2。cursor != sizehasNext() 判斷還有元素,繼續迭代,其實已經沒有元素了。
  • 繼續迭代,調用 next() 方法,此時 expectedModCount != modCount,直接拋出異常。
循環次數 cursor modCount expectedModCount size
1 1 3 3 3
2 2 3 3 3
3 3 4 3 2

再來看看刪除 kotlin 的執行流程:

  • 執行完第一次循環,cursor 爲 1,未產生刪除操做,modCount 爲 3,expectedModCount 爲 3,size 爲 3。cursor != sizehasNext() 判斷還有元素。
  • 執行完第二次循環,cursor 爲 2,產生刪除操做,modCount 爲 4,expectedModCount 爲 3,size 爲 2。cursor == sizehasNext() 判斷沒有元素了,再也不調用 next() 方法。

並非 fail-fast 失效了,僅僅只是剛好 cursor == sizehasNext() 方法誤覺得集合中已經沒有元素了,其實還有一個元素。循環兩次以後就終止循環了,再也不調用 next() 方法,也就不存在併發檢測了。

循環次數 cursor modCount expectedModCount size
1 1 3 3 3
2 2 3 3 2

本文由一個 ConcurrentModificationException 的例子,順藤摸瓜,解析了 ArrayList 迭代器的源碼,同時說明了 Java 集合框架的 fail-fast 機制。最後也驗證了加強型 for 循環中刪除元素並非百分之百會觸發 fail-fast

ArrayList 就說到這裏了,下一篇來看看 List 中一樣重要的 LinkedList

文章首發微信公衆號: 秉心說 , 專一 Java 、 Android 原創知識分享,LeetCode 題解。

更多 JDK 源碼解析,掃碼關注我吧!

相關文章
相關標籤/搜索