業(yè)務(wù)開(kāi)發(fā)時(shí)常常要對(duì) List 做切片處理,即取出其中部分元素構(gòu)成一個(gè)新的 List,我們通常會(huì)想到使用 List.subList 方法。但,和 Arrays.asList 的問(wèn)題類似,List.subList 返回的子 List 不是一個(gè)普通的 ArrayList。這個(gè)子 List 可以認(rèn)為是原始 List 的視圖,會(huì)和原始 List 相互影響。如果不注意,很可能會(huì)因此產(chǎn)生 OOM 問(wèn)題。接下來(lái),我們就一起分析下其中的坑。
如下代碼所示,定義一個(gè)名為 data 的靜態(tài) List 來(lái)存放 Integer 的 List,也就是說(shuō) data 的成員本身是包含了多個(gè)數(shù)字的 List。循環(huán) 1000 次,每次都從一個(gè)具有 10 萬(wàn)個(gè) Integer 的 List 中,使用 subList 方法獲得一個(gè)只包含一個(gè)數(shù)字的子 List,并把這個(gè)子 List 加入 data 變量:
private static List<List<Integer>> data = new ArrayList<>();
private static void oom() {
for (int i = 0; i < 1000; i++) {
List<Integer> rawList = IntStream.rangeClosed(1, 100000).boxed().collect(Collectors.toList());
data.add(rawList.subList(0, 1));
}
}
你可能會(huì)覺(jué)得,這個(gè) data 變量里面最終保存的只是 1000 個(gè)具有 1 個(gè)元素的 List,不會(huì)占用很大空間,但程序運(yùn)行不久就出現(xiàn)了 OOM:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3181)
at java.util.ArrayList.grow(ArrayList.java:265)
出現(xiàn) OOM 的原因是,循環(huán)中的 1000 個(gè)具有 10 萬(wàn)個(gè)元素的 List 始終得不到回收,因?yàn)樗冀K被 subList 方法返回的 List 強(qiáng)引用。那么,返回的子 List 為什么會(huì)強(qiáng)引用原始的 List,它們又有什么關(guān)系呢?我們?cè)倮^續(xù)做實(shí)驗(yàn)觀察一下這個(gè)子 List 的特性。
首先初始化一個(gè)包含數(shù)字 1 到 10 的 ArrayList,然后通過(guò)調(diào)用 subList 方法取出 2、3、4;隨后刪除這個(gè) SubList 中的元素?cái)?shù)字 3,并打印原始的 ArrayList;最后為原始的 ArrayList 增加一個(gè)元素?cái)?shù)字 0,遍歷 SubList 輸出所有元素:
List<Integer> list = IntStream.rangeClosed(1, 10).boxed().collect(Collectors.toList());
List<Integer> subList = list.subList(1, 4);
System.out.println(subList);
subList.remove(1);
System.out.println(list);
list.add(0);
try {
subList.forEach(System.out::println);
} catch (Exception ex) {
ex.printStackTrace();
}
代碼運(yùn)行后得到如下輸出:
[2, 3, 4]
[1, 2, 4, 5, 6, 7, 8, 9, 10]
java.util.ConcurrentModificationException
at java.util.ArrayList$SubList.checkForComodification(ArrayList.java:1239)
at java.util.ArrayList$SubList.listIterator(ArrayList.java:1099)
at java.util.AbstractList.listIterator(AbstractList.java:299)
at java.util.ArrayList$SubList.iterator(ArrayList.java:1095)
at java.lang.Iterable.forEach(Iterable.java:74)
可以看到兩個(gè)現(xiàn)象:
原始 List 中數(shù)字 3 被刪除了,說(shuō)明刪除子 List 中的元素影響到了原始 List;
嘗試為原始 List 增加數(shù)字 0 之后再遍歷子 List,會(huì)出現(xiàn) ConcurrentModificationException。
我們分析下 ArrayList 的源碼,看看為什么會(huì)是這樣。
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
protected transient int modCount = 0;
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
public void add(int index, E element) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // Increments modCount!!
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
public List<E> subList(int fromIndex, int toIndex) {
subListRangeCheck(fromIndex, toIndex, size);
return new SubList(this, offset, fromIndex, toIndex);
}
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;
}
public E set(int index, E element) {
rangeCheck(index);
checkForComodification();
return l.set(index+offset, element);
}
public ListIterator<E> listIterator(final int index) {
checkForComodification();
...
}
private void checkForComodification() {
if (ArrayList.this.modCount != this.modCount)
throw new ConcurrentModificationException();
}
...
}
}
第一,ArrayList 維護(hù)了一個(gè)叫作 modCount 的字段,表示集合結(jié)構(gòu)性修改的次數(shù)。所謂結(jié)構(gòu)性修改,指的是影響 List 大小的修改,所以 add 操作必然會(huì)改變 modCount 的值。
第二,分析第 21 到 24 行的 subList 方法可以看到,獲得的 List 其實(shí)是內(nèi)部類 SubList,并不是普通的 ArrayList,在初始化的時(shí)候傳入了 this。
第三,分析第 26 到 39 行代碼可以發(fā)現(xiàn),這個(gè) SubList 中的 parent 字段就是原始的 List。SubList 初始化的時(shí)候,并沒(méi)有把原始 List 中的元素復(fù)制到獨(dú)立的變量中保存。我們可以認(rèn)為 SubList 是原始 List 的視圖,并不是獨(dú)立的 List。雙方對(duì)元素的修改會(huì)相互影響,而且 SubList 強(qiáng)引用了原始的 List,所以大量保存這樣的 SubList 會(huì)導(dǎo)致 OOM。
第四,分析第 47 到 55 行代碼可以發(fā)現(xiàn),遍歷 SubList 的時(shí)候會(huì)先獲得迭代器,比較原始 ArrayList modCount 的值和 SubList 當(dāng)前 modCount 的值。獲得了 SubList 后,我們?yōu)樵?List 新增了一個(gè)元素修改了其 modCount,所以判等失敗拋出 ConcurrentModificationException 異常。
既然 SubList 相當(dāng)于原始 List 的視圖,那么避免相互影響的修復(fù)方式有兩種:
一種是,不直接使用 subList 方法返回的 SubList,而是重新使用 new ArrayList,在構(gòu)造方法傳入 SubList,來(lái)構(gòu)建一個(gè)獨(dú)立的 ArrayList;
另一種是,對(duì)于 Java 8 使用 Stream 的 skip 和 limit API 來(lái)跳過(guò)流中的元素,以及限制流中元素的個(gè)數(shù),同樣可以達(dá)到 SubList 切片的目的。
//方式一:
List<Integer> subList = new ArrayList<>(list.subList(1, 4));
//方式二:
List<Integer> subList = list.stream().skip(1).limit(3).collect(Collectors.toList());
修復(fù)后代碼輸出如下:
[2, 3, 4]
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
2
4
可以看到,刪除 SubList 的元素不再影響原始 List,而對(duì)原始 List 的修改也不會(huì)再出現(xiàn) List 迭代異常。