再說Java集合,subList之於ArrayList

工匠初心發表於2019-06-29

上一章說了很多ArrayList相關的內容,但還有一塊兒內容沒說到,那就是subList方法。先看一段程式碼

public static void testSubList() {
    List<String> stringList = new ArrayList<>();
    stringList.add("牛魔王");
    stringList.add("蛟魔王");
    stringList.add("鵬魔王");
    stringList.add("獅駝王");
    stringList.add("獼猴王");
    stringList.add("禺賊王");
    stringList.add("美猴王");

    List<String> substrings = stringList.subList(3,5);
    System.out.println(substrings.toString());
    System.out.println(substrings.size());
    substrings.set(1, "豬八戒");
    System.out.println(substrings.toString());
    System.out.println(stringList.toString());
}

看看執行結果如何?

[獅駝王, 獼猴王]
2
[獅駝王, 豬八戒]
[牛魔王, 蛟魔王, 鵬魔王, 獅駝王, 豬八戒, 禺賊王, 美猴王]

第一和第二的執行結果,非常容易理解,subList()方法作用就是擷取集合stringList中一個範圍內的元素。

第三和第四的執行結果都值得分析了,首先擷取的字串集合值為 [獅駝王, 獼猴王] ,但因為獼猴王在大雷音寺被美猴王打死了,我們用豬八戒來代替獼猴王;

因此我們通過substrings.set(1, "豬八戒"),將這個集合中第二個位置的值“獼猴王”設定為“豬八戒”,最終列印出來的結果也正是我們所預期的;但同時我們列印原集合stringList,發現其中的“獼猴王”也變成了“豬八戒”。這就比較奇怪了,兩個問題:

1.我們操作的是擷取後的集合,為什麼原集合會變?

2.我們設定擷取後某個位置(如第2個位置)的值,原集合改變的卻不是對應位置的值?

一. subList原理初探

接下來我們帶著問題尋找答案,我們看一下subList()的原始碼

/**
 * Returns a view of the portion of this list between the specified
 * {@code fromIndex}, inclusive, and {@code toIndex}, exclusive.  (If 
 * {@code fromIndex} and {@code toIndex} are equal, the returned list is 
 * empty.)  The returned list is backed by this list, so non-structural
 * changes in the returned list are reflected in this list, and vice-versa.
 * The returned list supports all of the optional list operations.
 *
 * <p>This method eliminates the need for explicit range operations (of
 * the sort that commonly exist for arrays).  Any operation that expects
 * a list can be used as a range operation by passing a subList view
 * instead of a whole list.  For example, the following idiom
 * removes a range of elements from a list:
 * <pre>
 *      list.subList(from, to).clear();
 * </pre>
 * Similar idioms may be constructed for {@link #indexOf(Object)} and
 * {@link #lastIndexOf(Object)}, and all of the algorithms in the
 * {@link Collections} class can be applied to a subList.
 *
 * <p>The semantics of the list returned by this method become undefined if
 * the backing list (i.e., this list) is <i>structurally modified</i> in
 * any way other than via the returned list.  (Structural modifications are
 * those that change the size of this list, or otherwise perturb it in such
 * a fashion that iterations in progress may yield incorrect results.)
 *
 * @throws IndexOutOfBoundsException {@inheritDoc}
 * @throws IllegalArgumentException {@inheritDoc}
 */
public List<E> subList(int fromIndex, int toIndex) {
    subListRangeCheck(fromIndex, toIndex, size);
    return new SubList(this, 0, fromIndex, toIndex);
}

看註釋,大概有以下幾個意思

  1. 返回的是原集合在fromIndex和toIndex之間的元素的檢視,雖然為檢視,但支援集合的所有方法;
  2. 當fromIndex和toIndex相同時,返回空的檢視;
  3. 任何對擷取的檢視的操作都會被原集合所取代;

看註釋僅能知道我們例子最後的執行結果是正常的,但是對原理也還並不是特別清楚。我們繼續看原始碼。

首先我們在例子中呼叫subList(3, 5)時,是new了一個SubList,這個SubList是ArrayList內部類,繼承了AbstractList

private class SubList extends AbstractList<E> implements RandomAccess {
    private final AbstractList<E> parent;
    private final int parentOffset;
    private final int offset;
    int size;

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

從這個內部類的原始碼中,我們可以看到:

  1. SubList並沒有像ArrayList一樣定義Object[]來存放資料,而定義了一個變數parent來儲存傳遞的原集合;
  2. 定義了一個offset用於儲存進行偏移量,當對SubList修改時,就可以通過偏移量找到parent中對應的位置;
  3. 定義了size用來表示我們在parent集合中可見範圍是多少;

再說Java集合,subList之於ArrayList

有了上面的說明,其實SubList的原理已經很清晰了,接下來,我們用SubList中常用的方法來印證一下。

二. add(E e)方法

substrings.add("九頭蛇");
System.out.println(substrings.toString());
System.out.println(stringList.toString());

接著上面的例子,在substrings中新增“九頭蛇”,按照規則,add()方法新增元素會在集合的最後,也就是說substrings的第3個位置(下標為2),對應parent原集合的位置下標就是2+3=5,會在stringList第六個位置插入“九頭蛇”。看一下輸出的結果

[獅駝王, 豬八戒, 九頭蛇]
[牛魔王, 蛟魔王, 鵬魔王, 獅駝王, 豬八戒, 九頭蛇, 禺賊王, 美猴王]

可以看到結果的確如此,那麼我們在看一下add(E e),在SubList這個內部類裡面並沒有發現該方法,因此我去父類中找。

在AbstractList中找到了

public boolean add(E e) {
    add(size(), e);
    return true;
}

接下來,我們在SubList中找到了實現方法

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

很明顯,原始碼和我們開始的分析是一致的,當然在新增之間需要進行空間容量判斷,是否足以新增新的元素,擴容規則,我們上一章已經講過。

三. 其他方法

關於SubList的其他方法,其實和add原理一樣,不論是set(int index, E e),get(int index),addAll(Collection<? extends E> c),remove(int index),都是先判斷當前傳入的位置索引是否正確(如是否大於size,小於0等),再根據規則計算出原集合中的位置下標,最終完成對集合的操作。

四. 總結

本文續接上一章ArrayList原理及使用,對ArrayList中的常用方法subList進行了剖析,從原始碼的角度對通過subList方法得到的集合和原集合有何關係,有何不同點,從而避免工作中遇到各種坑,若有不對之處,請批評指正,望共同進步,謝謝!

相關文章