從源碼角度解析ArrayList.subList的幾個坑

前言

《阿里巴巴Java開發手冊》中提出瞭如下幾點建議和規則:java

「規則1:」web

「規則2:」本文經過源碼分析,給你們講清楚《手冊》爲何這麼規定面試

ArrayList的subList分析

首先經過 IDEA 的提供的類圖工具,咱們能夠查看下該類的繼承體系。數組

具體步驟:在 SubList 類中 右鍵,選擇 「Diagrams」 -> 「Show Diagram」 。能夠看到 SubList 和 ArrayList 的繼承體系很是相似,都實現了 RandomAccess 接口 繼承自 AbstarctList。微信

可是SubList 和 ArrayList 並無繼承關係,所以 ArrayList 的 SubList 並不能強轉爲 ArrayList 。併發

「下面咱們寫一個簡單的測試代碼片斷來驗證轉換異常問題:」dom

public static void main(String[] args) {
    List<Integer> integerList = new ArrayList<>();
    integerList.add(0);
    integerList.add(1);
    integerList.add(2);
    List<Integer> subList = integerList.subList(01);

    ArrayList<Integer> castList = (ArrayList<Integer>) subList;
  }

輸出:編輯器

Exception in thread "main" java.lang.ClassCastException: java.util.ArrayList$SubList cannot be cast to java.util.ArrayList

從上面的結果也能夠清晰地看出,subList 並非 ArrayList 類型的實例,不能強轉爲 ArrayList 。函數

「咱們再來寫一個代碼片斷:」工具

public static void main(String[] args) {
    List<String> stringList = new ArrayList<>();
    stringList.add("月");
    stringList.add("伴");
    stringList.add("小");
    stringList.add("飛");
    stringList.add("魚");
    stringList.add("哈");
    stringList.add("哈");

    List<String> subList = stringList.subList(24);
    System.out.println("子列表:" + subList.toString());
    System.out.println("子列表長度:" + subList.size());

    subList.set(1"周星星");
    System.out.println("子列表:" + subList.toString());
    System.out.println("原始列表:" + stringList.toString());
  }

輸出:

子列表:[小, 飛]
子列表長度:2
子列表:[小, 周星星]
原始列表:[月, 伴, 小, 周星星, 魚, 哈, 哈]

能夠觀察到,對子列表的修改最終對原始列表產生了影響。

「那麼爲啥修改子序列的索引爲 1 的值影響的是原始列表的第 4 個元素呢?」

下面將進行源碼分析和解讀。

源碼分析

能夠看到SubList是ArrayList的一個內部類

這個 SubList 中的 parent 字段就是原始的 List。咱們 能夠認爲 SubList 是原始 List 的視圖

看看java.util.ArrayList#subList 源碼:這裏在構造子列表對象的時候傳入了 this,建立了SubList類

同時從上面註釋中咱們能夠學到如下知識點:

  • 該方法返回本列表中 fromIndex (包含)和 toIndex (不包含)之間的元素視圖。若是兩個索引相等會返回一個空列表。

  • 若是須要對 list 的某個範圍的元素進行操做,能夠用 subList,如:list.subList(from, to).clear();

  • 任何對子列表的操做最終都會反映到原列表中。

咱們再來看下SubList的構造函數

SubList(AbstractList<E> parent,
                int offset, int fromIndex, int toIndex) {
            this.parent = parent;
            this.parentOffset = fromIndex;
            this.offset = offset + fromIndex;
            this.size = toIndex - fromIndex;
            this.modCount = ArrayList.this.modCount;
        }

經過子列表的構造函數咱們知道,這裏的偏移量 ( offset ) 的值爲 fromIndex 參數,由於參數傳的offset等於0

接下來咱們再看下函數 java.util.ArrayList.SubList#set 源碼:

public E set(int index, E e) {
    rangeCheck(index);
    checkForComodification();
    E oldValue = ArrayList.this.elementData(offset + index);
    ArrayList.this.elementData[offset + index] = e;
    return oldValue;
}

能夠看到替換值的時候,獲取索引是經過 offset + index 計算得來的。

這裏的 java.util.ArrayList#elementData 即爲原始列表存儲元素的數組。

所以上面提到的:

爲啥子序列的索引爲 1 的值影響的是原始列表的第 4 個元素呢?

這個問題就解答了

「到如今完成了規則1的解釋了」

另外在 SubList 的構造函數中,咱們發現會將 ArrayList 的 modCount 賦值給 SubList 的 modCount 。

「因此這個就引出了另一個問題」

咱們先看 java.util.ArrayList#add(E)的源碼:

public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

能夠發現新增元素和刪除元素,都會對 modCount 進行修改。

咱們來看下 SubList 的 核心的函數 java.util.ArrayList.SubList#get

public E get(int index) {
            rangeCheck(index);
            checkForComodification();
            return ArrayList.this.elementData(offset + index);
        }

會進行修改檢查:

private void checkForComodification() {
            if (ArrayList.this.modCount != this.modCount)
                throw new ConcurrentModificationException();
        }

咱們再來看下 SubList 的 核心的函數 java.util.ArrayList.SubList#add

public void add(int index, E e) {
            rangeCheckForAdd(index);
            checkForComodification();
            parent.add(parentOffset + index, e);
            this.modCount = parent.modCount;
            this.size++;
        }

也會調用checkForComodification進行檢查

從上面的 SubList 的構造函數咱們能夠知道,SubList 複製了 ArrayList 的 modCount,所以對原函數的新增或刪除都會致使 ArrayList 的 modCount 的變化。

而子列表的遍歷、增長、刪除時又會檢查建立 SubList 時的 modCount 是否一致,顯然此時二者會不一致,致使拋出 ConcurrentModificationException (併發修改異常)。

「至此上面的規則2的緣由咱們也清楚了。」

「既然 SubList 至關於原始 List 的視圖,那麼避免相互影響的修復方式有兩種:」

一種是,不直接使用 subList 方法返回的 SubList,而是從新使用 new ArrayList,在構 造方法傳入 SubList,來構建一個獨立的 ArrayList;

List<Integer> subList = new ArrayList<>(list.subList(14));

另外一種是,對於 Java 8 使用 Stream 的 skip 和 limit API 來跳過流中的元素,以及限制 流中元素的個數,一樣能夠達到 SubList 切片的目的。

List<Integer> subList = list.stream().skip(1).limit(3).collect(Collectors.toList());

總結

本文經過類圖分析、源碼分析以及的方式對 ArrayList 的 SubList 問題進行分析

「要點:」

  • ArrayList 內部類 SubList 和 ArrayList 沒有繼承關係,所以沒法將其強轉爲 ArrayList 。
  • subList()返回的是 ArrayList 的內部類 SubList,並非 ArrayList 自己,而是 ArrayList 的一個視 圖,對於 SubList 的全部操做最終會反映到原列表上
  • ArrayList 的 SubList 構造時傳入 ArrayList 的 modCount,所以對原列表的修改將會致使子列表的遍歷、增長、刪除產生 ConcurrentModificationException 異常。

「學習建議:」

學習不能浮於表面,不能知其然而不知其因此然。而看源碼是掌握深度知識最好的方法。

但願你們從如今開始學習和開發中可以偶爾到感興趣的類中查看源碼,這樣學的更快,更紮實。經過進入源碼中自主研究,這樣印象更加深入,掌握的程度更深。

「以爲有收穫,能夠幫忙點贊,在看哈,您的點贊,轉發對小飛魚很是重要,謝謝」

最後

微信搜索:月伴飛魚,交個朋友

1.平常分享一篇實用的技術文章,對面試,工做都有幫助

2.後臺回覆666,得到免費電子書籍,會持續更新

條件語句的多層嵌套問題優化,助你寫出不讓同事吐槽的代碼


ConcurrentHashMap核心原理,此次完全給整明白了


ReentrantLock核心原理,絕對乾貨


最完整的Explain總結,SQL優化再也不困難

本文分享自微信公衆號 - 月伴飛魚(gh_c4183eee9eb9)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索