1. 程式人生 > 實用技巧 >java集合原始碼分析(二):List與AbstractList

java集合原始碼分析(二):List與AbstractList

概述

上一篇文章基本介紹了 List 介面的上層結構,也就是 Iterable 介面,Collection 介面以及實現了 Collection 介面的抽象類的基本情況,現在在前文的基礎上,我們將繼續向實現前進,進一步的探索 List 介面與其抽象實現類 AbstractList 的原始碼,瞭解他是如何在三大實現類與 Collection 介面之間實現承上啟下的作用的。

一、List 介面

List 介面繼承了 Collection 介面,在 Collection 介面的基礎上增加了一些方法。相對於 Collection 介面,我們可以很明顯的看到,List 中增加了非常多根據下標操作集合的方法

,我們可以簡單粗暴的分辨一個方法的抽象方法到底來自 Collection 還是 List:引數裡有下標就是來自 List,沒有就是來自 Collection。

可以說,List 介面在 Collection 的基礎上,進一步明確了 List 集合運允許根據下標快速存取的特性

1.新增的方法

  • get():根據下標獲取指定元素;
  • replaceAll():引數一個函式式介面UnaryOperator<E>,這個方法允許我們通過傳入的匿名實現類的方法去對集合中的每一個類做一些處理以後再放回去;
  • sort():對集合中的資料進行排序。引數是 Comparator<? super E>
    ,這個引數讓我們傳入一個比較的匿名方法,用於陣列排序;
  • set():用指定的元素替換集合中指定位置的元素;
  • indexOf():返回指定元素在此列表中首次出現的索引;如果此列表不包含該元素,則返回-1;
  • lastIndexOf():返回指定元素在此列表中最後一次出現的索引,否則返回-1;
  • listIterator():這個是個多型的方法。無參的 listIterator()用於獲取迭代器,而有參的 listIterator()可以傳入下標,從集合的指定位置開始獲取迭代器。指定的索引指示首次呼叫next將返回的第一個元素。
  • subList():返回此列表中指定的兩個指定下標之間的集合的檢視。注意,這裡說的是檢視,因而對檢視的操作會影響到集合,反之亦然。

2.同名的新方法

  • add():新增元素。List 中的 add() 引數的(int,E),而 Collection 中的 add() 引數是 E,因此 List 集合中同時存在指定下標和不指定下標兩種新增方式
  • remove():刪除指定下標的元素。注意,List 的 remove() 引數是 int ,而 Collection 中的 ``remove()` 引數是 Objce,也就是說,List 中同時存在根據元素是否相等和根據元素下標刪除元素兩種方式

3.重寫的方法

  • spliterator():List 介面重寫了 Collection 介面的預設實現,換成了根據順序的分割。

二、AbstractList 抽象類

AbstractList 類是一個繼承了 AbstractCollection 類並且實現了 List 介面的抽象類,它相當於在 AbstractCollection 後的第二層方法模板。是對 List 介面的初步實現,同時也是 Collection 的進一步實現。

1.不支援的實現

可以直接通過下標操作的set()add()remove()都是 List 引入的新介面,這些都 AbstractList 都不支援,要使用必須由子類重寫。

public E set(int index, E element) {
    throw new UnsupportedOperationException();
}
public void add(int index, E element) {
    throw new UnsupportedOperationException();
}
public E remove(int index) {
    throw new UnsupportedOperationException();
}

2.內部類們

跟 AbstractCollection 類不同,AbstractList 擁有幾個特別的內部類,他們分別的迭代器類:Itr 和 ListItr,對應獲取他們的方法是:

  • iterator():獲取 Itr 迭代器類;
  • listIterator():獲取 ListItr 迭代器類。這是個多型方法,可以選擇是否從指定下標開始,預設從下標為0的元素開始迭代;

檢視類 SubList 和 RandomAccessSubList:

  • subList():獲取檢視類,會自動根據實現類是否繼承 RandomAccess 而返回 SubList 或 RandomAccessSubList。

這些內部類同樣被一些其他的方法所依賴,所以要全面的瞭解 AbstractList 方法的實現,就需要先了解這些內部類的作用和實現原理。

三、subList方法與內部類

subList()算是一個比較常用的方法了,在 List 介面的規定中,這個方法應該返回一個當前集合的一部分的檢視:

public List<E> subList(int fromIndex, int toIndex) {
    // 是否是實現了RandomAccess介面的類
    return (this instanceof RandomAccess ?
            // 是就返回一個可以隨機訪問的內部類RandomAccessSubList
            new RandomAccessSubList<>(this, fromIndex, toIndex) :
            // 否則返回一個普通內部類SubList
            new SubList<>(this, fromIndex, toIndex));
}

這裡涉及到 RandomAccessSubList 和 SubList 這個內部類,其中,RandomAccessSubList 類是 SubList 類的子類,但是實現了 RandomAccess 介面。

1.SubList 內部類

我們可以簡單的把 SubList 和 AbstractList 理解為裝飾器模式的一種實現,就像 SynchronizedList 和 List 介面的實現類一樣。SubList 內部類通過對 AbstractList 的方法進行了再一次的封裝,把對 AbstractList 的操作轉變為了對 “檢視的操作”。

我們先看看 SubList 這個類的成員變數和構造方法:

class SubList<E> extends AbstractList<E> {
    // 把外部類AbstractList作為成員變數
    private final AbstractList<E> l;
    // 表示檢視的起始位置(偏移量)
    private final int offset;
    // SubList檢視的長度
    private int size;

    SubList(AbstractList<E> list, int fromIndex, int toIndex) {
        if (fromIndex < 0)
            throw new IndexOutOfBoundsException("fromIndex = " + fromIndex);
        if (toIndex > list.size())
            throw new IndexOutOfBoundsException("toIndex = " + toIndex);
        if (fromIndex > toIndex)
            throw new IllegalArgumentException("fromIndex(" + fromIndex +
                                               ") > toIndex(" + toIndex + ")");
        // 獲取外部類的引用
        // 這也是為什麼操作檢視或者外部類都會影響對方的原因,因為都操作記憶體中的同一個例項
        l = list;
        // 獲取當前檢視在外部類中的起始下標
        offset = fromIndex;
        // 當前檢視的長度就是外部類擷取的檢視長度
        size = toIndex - fromIndex;
        this.modCount = l.modCount;
    }
    
}

我們可以參考圖片理解一下:

然後 subList 裡面的方法就很好理解了:

public E set(int index, E element) {
    // 檢查下標是否越界
    rangeCheck(index);
    // 判斷是存在併發修改
    checkForComodification();
    // 把元素新增到偏移量+檢視下標的位置
    return l.set(index+offset, element);
}

其他方法都差不多,這裡便不再多費筆墨了。

2.RandomAccessSubList 內部類

然後是 SubList 的子類 RandomAccessSubList:

class RandomAccessSubList<E> extends SubList<E> implements RandomAccess {
    RandomAccessSubList(AbstractList<E> list, int fromIndex, int toIndex) {
        super(list, fromIndex, toIndex);
    }

    public List<E> subList(int fromIndex, int toIndex) {
        return new RandomAccessSubList<>(this, fromIndex, toIndex);
    }
}

我們可以看見,他實際上還是 SubList,但是實現了 RandomAccess 介面。關於這個介面,其實只是一個標記,實現了該介面的類可以實現快速隨機訪問(下標),通過 for 迴圈+下標取值會比用迭代器更快。

Vector 和 ArrayList 都實現了這個介面,而 LinkedList 沒有。專門做此實現也是為了在實現類呼叫的 subList()方法時可以分辨這三者。

四、iterator方法與內部類

在 AbstractList 裡面,為我們提供了 Itr 和 ListItr 兩種迭代器。

迭代器是 AbstractList 中很重要的一塊內容,他是對整個介面體系的頂層介面,也就是 Iterable 介面中的 iterator() 方法的實現,原始碼中的很多涉及遍歷的方法,都離不開內部實現的迭代器類。

1.迭代器的 fast-fail 機制

我們知道,AbstractList 預設是不提供執行緒安全的保證的,但是為了儘可能的避免併發修改對迭代帶來的影響,JDK 引入一種 fast-fail 的機制,即如果檢測的發生併發修改,就立刻丟擲異常,而不是讓可能出錯的引數被使用從而引發不可預知的錯誤。

對此,AbstractList 提供了一個成員變數 modCount,JavaDoc 是這麼描述它的:

已對該列表進行結構修改的次數。

結構修改是指更改列表大小或以其他方式干擾列表的方式,即正在進行的迭代可能會產生錯誤的結果。該欄位由iterator和listIterator方法返回的迭代器和列表迭代器實現使用。如果此欄位的值意外更改,則迭代器(或列表迭代器)將丟擲ConcurrentModificationException,以響應下一個,移除,上一個,設定或新增操作。

面對迭代期間的併發修改,這提供了快速失敗的行為,而不是不確定的行為。

子類對此欄位的使用是可選的。如果子類希望提供快速失敗的迭代器(和列表迭代器),則只需在其add(int,E)和remove(int)方法(以及任何其他覆蓋該方法導致結構化的方法)中遞增此欄位即可)。

一次呼叫add(int,E)或remove(int)不得在此欄位中新增不超過一個,否則迭代器(和列表迭代器)將丟擲虛假的ConcurrentModificationExceptions。

如果實現不希望提供快速失敗迭代器,則可以忽略此欄位。

這個時候我們再回去看看迭代器類 Itr 的一部分程式碼,可以看到:

private class Itr implements Iterator<E> {
    // 迭代器認為後備列表應該具有的modCount值。如果違反了此期望,則迭代器已檢測到併發修改。
    int expectedModCount = modCount;
    
    // 檢查是否發生併發操作
    final void checkForComodification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
}

結合程式碼,我們就不難理解這個 fast-fail 機制是怎麼實現的了:

AbstractList 提供了一個成員變數用於記錄對集合結構性修改的次數,如果子類希望實現併發修改錯誤的檢查,就需要結構性操作的方法裡讓modCount+1。這樣。在獲取迭代器以後,迭代器內部會獲取當前的modCount賦值給expectedModCount

當使用迭代器迭代的時候,每一次迭代都會檢測modCountexpectedModCount是否相等。如果不相等,說明迭代器建立以後,集合結構被修改了,這個時候再去進行迭代可能會出現錯誤(比如少遍歷一個,多遍歷一個),因此檢測到後會直接丟擲 ConcurrentModificationException異常。

ListItr 繼承了 Itr ,因此他們都有一樣的 fast-fail機制。

值得一提的是,對於啟用了 fast-fail 機制的實現類,只有使用迭代器才能邊遍歷邊刪除,原因也是因為併發修改檢測:

2.Itr 迭代器

現在,回到 Itr 的程式碼上:

private class Itr implements Iterator<E> {
    // 後續呼叫next返回的元素索引
    int cursor = 0;

    // 最近一次呼叫返回的元素的索引。如果通過呼叫remove刪除了此元素,則重置為-1。
    int lastRet = -1;

    // 迭代器認為後備列表應該具有的modCount值。如果違反了此期望,則迭代器已檢測到併發修改。
    int expectedModCount = modCount;
	
    public boolean hasNext() {
        return cursor != size();
    }

    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();
        }
    }

    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();
    }
}

迭代方法

除了併發修改檢測外,迭代器迭代的方式也出乎意料。我們可以看看 hasNext()方法:

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();
    }
}

這個邏輯其實跟連結串列的遍歷是一樣的,只不過指標變成了陣列的下標。以連結串列的方式去理解:

我們把迴圈裡呼叫next()之後的節點叫做下一個節點,反正稱為當前節點。假如現在有 a,b,c 三個元素:

  • 當初始化的時候,指向最後一次操作的的節點的指標 lastRet=-1,即當前節點不存在,當前遊標 cursor=0,即指向下一個節點 a;
  • 當開始迭代的時候,把遊標的值賦給臨時指標 i,然後通過遊標獲取並返回下一個節點 a,再把遊標指向 a 的下一個節點 b,此時 cursor=1lastRet=-1i=1
  • 接著讓lastRet=i,也就是當前指標指向新的當前節點 a,現在 lastRet=0cursor=1`,完成了對第一個節點 a 的迭代;
  • 重複上述過程,把節點中的每一個元素都處理完。

現在我們知道了迭代的方式,cursorlastRet 的作用,也就不難理解 remove()方法了:

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

    try {
        // 呼叫刪除方法
        AbstractList.this.remove(lastRet);
        if (lastRet < cursor)
		   // 因為刪除了當前第i個節點,所以i+1個節點就會變成第i個節點,
            // 呼叫next()以後cursor會+1,因此如果不讓cursor-1,就會,next()以後跳過原本的第i+1個節點
            // 拿上面的例子來說,你要刪除abc,但是在刪除a以後會跳過b直接刪除c
            cursor--;
        // 最近一個操作的節點被刪除了,故重置為-1
        lastRet = -1;
        // 因為呼叫了外部類的remove方法,所以會改變modCount值,迭代器裡也要獲取最新的modCount
        expectedModCount = modCount;
    } catch (IndexOutOfBoundsException e) {
        throw new ConcurrentModificationException();
    }
}

至於hasNext()方法沒啥好說的,如果 cursor已經跟集合的長度一樣長了,說明就已經迭代到底了。

2.ListItr 迭代器

ListItr 繼承了 Itr 類,並且實現了 ListIterator 介面。其中,ListIterator 介面又繼承了 Iterator 介面。他們的類關係圖是這樣的:

ListIterator 介面在 Iterator 介面的基礎上,主要提供了六個新的抽象方法:

  • hasPrevious():是否有前驅節點;
  • previous():向前迭代;
  • nextIndex():獲取下一個元素的索引;
  • previousIndex():返回上一個元素的索引;
  • set():替換元素;
  • add():新增元素;

可以看出來,實現了 ListIterator 的 ListItr 類要比 Itr 更加強大,不但可以向後迭代,還能向前迭代,還可以在迭代過程中更新或者新增節點。

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;
            // 使用AbstractList的get方法獲取當前節點
            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();
        }
    }
}

這裡比較不好理解的是下一節點還有當前節點這個概念,其實可以這麼理解:cursor遊標指定的必定是下一次 next()操作要得到的節點,因此cursor在操作前或者操作後指向的必定就是下一節點,因此相對下一節點,cursor其實就是當前節點,相對下一節點來說就是上一節點。

也就是說,假如現在有 a,b,c 三個元素,現在的 cursor 為2,也就是指向 b。呼叫 next()以後遊標就會指向 c,而呼叫previous()以後遊標又會指回 b。

至於lastRet這個成員變數只是用於記錄最近一次操作的節點是哪個,跟方向性是無關。

五、AbstractList 實現的方法

1.add

注意,現在現在 AbstractList 的 add(int index, E e)仍然還不被支援,add(E e)只是定義了通過 add(int index, E e)把元素新增到隊尾的邏輯。

// 不指定下標的add,預設邏輯為新增到隊尾
public boolean add(E e) {
    add(size(), e);
    return true;
}

關於 AbstractList 和 AbstractCollection 中 add()方法之間的關係是這樣的:

AbstractList 這裡的 add(E e)就非常有模板方模式提到的“抽象類規定演算法骨架”這個感覺了。AbstractCollection 介面提供了 add(E e)的初步實現(儘管只是拋異常),然後到了 AbstractList 中就完善了 add(E e)方法的邏輯——通過呼叫 add(int index,E e)方法把元素插到隊尾,但是具體的 add(int index,E e)怎麼實現再交給子類決定。

2.indexOf/LastIndexOf

public int indexOf(Object o) {
    ListIterator<E> it = listIterator();
    if (o==null) {
        while (it.hasNext())
            if (it.next()==null)
                return it.previousIndex();
    } else {
        while (it.hasNext())
            if (o.equals(it.next()))
                return it.previousIndex();
    }
    return -1;
}

public int lastIndexOf(Object o) {
    ListIterator<E> it = listIterator(size());
    if (o==null) {
        while (it.hasPrevious())
            if (it.previous()==null)
                return it.nextIndex();
    } else {
        while (it.hasPrevious())
            if (o.equals(it.previous()))
                return it.nextIndex();
    }
    return -1;
}

3.addAll

這裡的addAll來自於List 集合的 addAll。引數是需要合併的集合跟起始下標:

public boolean addAll(int index, Collection<? extends E> c) {
    rangeCheckForAdd(index);
    boolean modified = false;
    for (E e : c) {
        add(index++, e);
        modified = true;
    }
    return modified;
}

這裡的 rangeCheckForAdd()方法是一個檢查下標是否越界的方法:

private void rangeCheckForAdd(int index) {
    // 不得小於0或者大於集合長度
    if (index < 0 || index > size())
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

4.removeRange

這個方法是 AbstractList 私有的方法,一般被子類用於刪除一段多個元素,實現上藉助了 ListIter 迭代器。

protected void removeRange(int fromIndex, int toIndex) {
    ListIterator<E> it = listIterator(fromIndex);
    // 從fromIndex的下一個開始,刪到toIndex
    for (int i=0, n=toIndex-fromIndex; i<n; i++) {
        it.next();
        it.remove();
    }
}

六、AbstractList 重寫的方法

1.equals

equals()方法比較特殊,他是來自於 Collection 和 List 介面中的抽象方法,在 AbstractList 得中實現,但是實際上也是對 Object 中方法的重寫。考慮到 equals()情況特殊,所以我們也認為它是一個重寫的方法。

我們可以先看看 JavaDoc 是怎麼說的:

比較指定物件與此列表是否相等。當且僅當指定物件也是一個列表,並且兩個列表具有相同的大小,並且兩個列表中所有對應的元素對相等時,才返回true

然後再看看原始碼是什麼樣的:

public boolean equals(Object o) {
    // 是否同一個集合
    if (o == this)
        return true;
    // 是否實現了List介面
    if (!(o instanceof List))
        return false;
	
    // 獲取集合的迭代器並同時遍歷
    ListIterator<E> e1 = listIterator();
    ListIterator<?> e2 = ((List<?>) o).listIterator();
    while (e1.hasNext() && e2.hasNext()) {
        E o1 = e1.next();
        Object o2 = e2.next();
        // 兩個集合中的元素是否相等
        if (!(o1==null ? o2==null : o1.equals(o2)))
            return false;
    }
    // 是否兩個集合長度相同
    return !(e1.hasNext() || e2.hasNext());
}

從原始碼也可以看出,AbstractList 的 equals() 是要求兩個集合絕對相等的:順序相等,並且相同位置的元素也要相等。

2.hashCode

hashCode()equals()情況相同。AbstractList 重新定義了 hashCode()

public int hashCode() {
    int hashCode = 1;
    for (E e : this)
        hashCode = 31*hashCode + (e==null ? 0 : e.hashCode());
    return hashCode;
}

新的計算方式會獲取集合中每一個元素的 hashCode 去計算集合的 hashCode,這可能是考慮到原本情況下,同一個集合哪怕裝入的元素不同也會獲得相同的 hashCode,可能會引起不必要的麻煩,因此重寫了次方法。

我們可以寫個測試看看:

List<String> list1 = new ArrayList<>();
list1.add("a");
System.out.println(list1.hashCode()); // 128
list1.add("c");
System.out.println(list1.hashCode()); // 4067

七、總結

List 介面繼承了 Collection 介面,新增方法的特點主要體現在可以通過下標去操作節點,可以說大部分下標可以作為引數的方法都是 List 中新增的方法。

AbstractList 是實現了 List 的抽象類,他實現了 List 介面中的大部分方法,同時他繼承了 AbstractCollection ,沿用了一些 AbstractCollection 中的實現。這兩個抽象類可以看成是模板方法模式的一種體現。

他提供了下標版的 add()remove()set()的空實現。

AbstractList 內部提供兩個迭代器,Itr 和 ListItr,Itr 實現了 Iterator介面,實現了基本的迭代刪除,而 ListItr 實現了ListIterator,在前者的基礎上增加了迭代中新增修改,以及反向迭代的相關方法,並且可以從指定的位置開始建立迭代器。

AbstractList 的 SubList 可以看成 AbstractList 的包裝類,他在例項化的時候會把外部類例項的引用賦值給成員變數,同名的操作方法還仍然是呼叫 AbstractList 的,但是基於下標的呼叫會在預設引數的基礎上加上步長,以實現一種類似“檢視”的感覺。

AbstractList 引入了併發修改下 fast-fail 的機制,在內部維護一個成員變數 modelCount,預設為零,每次結構性修改都會讓其+1。在迭代過程中會預設檢查 modelCount是否符合預期值,否則丟擲異常。值得注意的是,這個需要實現類的配合,在實現 add()等方法的時候要讓 modelCount+1。對於一些實現類,在迭代中刪除可能會丟擲 ConcurrentModificationExceptions,就是這方面的問題。

AbstractList 重寫了 hashCode()方法,不再直接獲取例項的 HashCode 值,而遍歷集合,根據每一個元素的 HashCode 計算集合的 HashCode,這樣保證了內容不同的相同集合不會得到相同的 HashCode。