Java|List.subList 踩坑小記
很久以前在使用 Java 的 List.subList 方法時踩過一個坑,當時記了一條待辦,要寫一寫這事,今天完成它。
我們先來看一段代碼:
// 初始化 list 為 { 1, 2, 3, 4, 5 }
List<Integer> list = new ArrayList<>();
for (int i = 1; i <= 5; i++) {
list.add(i);
}
// 取前 3 個元素作為 subList,操作 subList
List<Integer> subList = list.subList(0, 3);
subList.add(6);
System.out.println(list.size());
輸出是 5 還是 6?
沒踩過坑的我,會回答是 5,理由是:往一個 List 里加元素,關其它 List 什么事?
而掉過坑的我,口中直呼 666。
好了不繞彎子,我們直接看下 List.subList 方法的注釋文檔:
/**
* Returns a view of the portion of this list between the specified
* <tt>fromIndex</tt>, inclusive, and <tt>toIndex</tt>, exclusive. (If
* <tt>fromIndex</tt> and <tt>toIndex</tt> 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 supported
* by this list.<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>{@code
* list.subList(from, to).clear();
* }</pre>
* Similar idioms may be constructed for <tt>indexOf</tt> and
* <tt>lastIndexOf</tt>, and all of the algorithms in the
* <tt>Collections</tt> 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.)
*
* @param fromIndex low endpoint (inclusive) of the subList
* @param toIndex high endpoint (exclusive) of the subList
* @return a view of the specified range within this list
* @throws IndexOutOfBoundsException for an illegal endpoint index value
* (<tt>fromIndex < 0 || toIndex > size ||
* fromIndex > toIndex</tt>)
*/
List<E> subList(int fromIndex, int toIndex);
這里面有幾個要點:
subList 返回的是原 List 的一個 視圖,而不是一個新的 List,所以對 subList 的操作會反映到原 List 上,反之亦然;
如果原 List 在 subList 操作期間發生了結構修改,那么 subList 的行為就是未定義的(實際表現為拋異常)。
第一點好理解,看到「視圖」這個詞相信大家就都能理解了。我們甚至可以結合 ArrayList 里的 SubList 子類源碼進一步看下:
private class SubList extends AbstractList<E> implements RandomAccess {
private final AbstractList<E> parent;
// ...
SubList(AbstractList<E> parent,
int offset, int fromIndex, int toIndex) {
this.parent = parent;
// ...
this.modCount = ArrayList.this.modCount;
}
public E set(int index, E e) {
// ...
checkForComodification();
// ...
ArrayList.this.elementData[offset + index] = e;
// ...
}
public E get(int index) {
// ...
checkForComodification();
return ArrayList.this.elementData(offset + index);
}
public void add(int index, E e) {
// ...
checkForComodification();
parent.add(parentOffset + index, e);
this.modCount = parent.modCount;
// ...
}
public E remove(int index) {
// ...
checkForComodification();
E result = parent.remove(parentOffset + index);
this.modCount = parent.modCount;
// ...
}
private void checkForComodification() {
if (ArrayList.this.modCount != this.modCount)
throw new ConcurrentModificationException();
}
// ...
}
可以看到幾乎所有的讀寫操作都是映射到 ArrayList.this、或者 parent(即原 List)上的,包括 size、add、remove、set、get、removeRange、addAll 等等。
第二點,我們在文首的示例代碼里加上兩句代碼看現象:
list.add(0, 0);
System.out.println(subList);
System.out.println 會拋出異常 java.util.ConcurrentModificationException。
我們還可以試下,在聲明 subList 后,如果對原 List 進行元素增刪操作,然后再讀寫 subList,基本都會拋出此異常。
因為 subList 里的所有讀寫操作里都調用了 checkForComodification(),這個方法里檢驗了 subList 和 List 的 modCount 字段值是否相等,如果不相等則拋出異常。
modCount 字段定義在 AbstractList 中,記錄所屬 List 發生 結構修改 的次數。結構修改 包括修改 List 大小(如 add、remove 等)、或者會使正在進行的迭代器操作出錯的修改(如 sort、replaceAll 等)。
好了小結一下,這其實不算是坑,只是 不應該僅憑印象和猜測,就開始使用一個方法,至少花一分鐘認真讀完它的官方注釋文檔。