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

概述

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

一、List 接口

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

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

image-20201126114026855

然後 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 的類關係圖

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()方法之間的關係是這樣的:

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。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章