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

概述

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

1、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 接口的默認實現,換成了根據順序的分割。

2、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,對應獲取他們的方法是:dom

  • iterator():獲取 Itr 迭代器類;
  • listIterator():獲取 ListItr 迭代器類。這是個多態方法,能夠選擇是否從指定下標開始,默認從下標爲0的元素開始迭代;

視圖類 SubList 和 RandomAccessSubList:函數

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

這些內部類一樣被一些其餘的方法所依賴,因此要全面的瞭解 AbstractList 方法的實現,就須要先了解這些內部類的做用和實現原理。測試

3、subList方法與內部類

subList()算是一個比較經常使用的方法了,在 List 接口的規定中,這個方法應該返回一個當前集合的一部分的視圖:this

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()方法時能夠分辨這三者。

4、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這個成員變量只是用於記錄最近一次操做的節點是哪一個,跟方向性是無關。

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

6、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

7、總結

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。

相關文章
相關標籤/搜索