上一章說了很多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);
}
看註釋,大概有以下幾個意思
- 返回的是原集合在fromIndex和toIndex之間的元素的檢視,雖然為檢視,但支援集合的所有方法;
- 當fromIndex和toIndex相同時,返回空的檢視;
- 任何對擷取的檢視的操作都會被原集合所取代;
看註釋僅能知道我們例子最後的執行結果是正常的,但是對原理也還並不是特別清楚。我們繼續看原始碼。
首先我們在例子中呼叫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;
}
}
從這個內部類的原始碼中,我們可以看到:
- SubList並沒有像ArrayList一樣定義Object[]來存放資料,而定義了一個變數parent來儲存傳遞的原集合;
- 定義了一個offset用於儲存進行偏移量,當對SubList修改時,就可以通過偏移量找到parent中對應的位置;
- 定義了size用來表示我們在parent集合中可見範圍是多少;
有了上面的說明,其實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方法得到的集合和原集合有何關係,有何不同點,從而避免工作中遇到各種坑,若有不對之處,請批評指正,望共同進步,謝謝!