在上文ArrayList源码分析(基于jdk1.8)(一):源码及基本操作中对ArrayList源码进行了分析,那么最近在阅读阿里代码规范的时候,发现对asList方法有特别的约定,这个方法也可能是我们经常会出现问题的地方。
1.问题重现
现有案例如下,假定我们有ArrayList数组,存储了A-G,那么随后,用subList将ABC取出,将B改成H,之后再继续给ArrayList中增加元素,则会报错。
代码如下:
public static void main(String[] args) {
List list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
list.add("D");
list.add("E");
list.add("F");
list.add("G");
List sub = list.subList(0,3);
System.out.println("1:"+sub);
sub.set(1,"H");
System.out.println("2:"+sub);
System.out.println("3:"+list);
list.add("K");
sub.set(1,"I");
System.out.println("4:"+sub);
System.out.println("5:"+list);
}
执行结果如下:
1:[A, B, C]
2:[A, H, C]
3:[A, H, C, D, E, F, G]
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$SubList.checkForComodification(ArrayList.java:1239)
at java.util.ArrayList$SubList.set(ArrayList.java:1035)
at com.dhb.ArrayList.test.AsListTest.main(AsListTest.java:25)
2.源码分析
可以看到,在subList产生的新的子集之后,我们对subList进行了set操作,之后再对list本身执行操作。这周还再次操作sub的时候就出现了ConcurrentModificationException。对于ConcurrentModificationException异常,我们在前文对ArrayList源码进行分析的时候说过,如果fail-fast机制被触发的时候,就会产生这个异常。
2.1 subList方法源码
在ArrayList中,subList是ArrayList的一个内部类。
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.
*
* 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:
*
* list.subList(from, to).clear();
*
* 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.
*
* The semantics of the list returned by this method become undefined if
* the backing list (i.e., this list) is structurally modified 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 subList(int fromIndex, int toIndex) {
subListRangeCheck(fromIndex, toIndex, size);
return new SubList(this, 0, fromIndex, toIndex);
}
可以看到,这个类返回了个内部类SubList。其注释大意为,返回一个指定索引的视图。返回的视图其底层结构还是ArrayList本身,任何对返回视图的结构操作,都会体现到底层的ArrayList中,反之亦然。也就是说,对与subList这个视图,我们只要执行了任何set、add、remove的操作,实际上是对底层ArrayList进行的修改。那么结合之前对fail-fast机制的了解,这个操作肯定会将底层ArrayList中控制fail-fast的modCount加1。而在SubList本身的代码中,这个modCount则不变,在check的时候肯定会导致ConcurrentModificationException产生。
2.2 SubList类源码
private class SubList extends AbstractList implements RandomAccess {
private final AbstractList parent;
private final int parentOffset;
private final int offset;
int size;
SubList(AbstractList 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其本身是继承了AbstractList并实现了RandomAccess接口。因此在很多情况下,我们如果希望将得到的subList直接按ArrayList进行使用,则是肯定不正确的。
List list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
list.add("D");
list.add("E");
list.add("F");
list.add("G");
List sub = list.subList(0,3);
ArrayList list1 = (ArrayList) list;
上述操作将导致异常:
Exception in thread "main" java.lang.ClassCastException: java.util.ArrayList$SubList cannot be cast to java.util.ArrayList
at com.dhb.ArrayList.test.AsListTest.main(AsListTest.java:29)
2.3 SubList的fail-fast检测机制
我们查看add方法:
public void add(int index, E e) {
rangeCheckForAdd(index);
checkForComodification();
parent.add(parentOffset + index, e);
this.modCount = parent.modCount;
this.size++;
}
在所有相关的方法调用之前都会执行checkForComodification:
private void checkForComodification() {
if (ArrayList.this.modCount != this.modCount)
throw new ConcurrentModificationException();
}
那么在所有SubList内部的方法执行之前,都要进行check操作。这个检测以ArrayList中的modCount和SubList中的modCount进行比较。那么我们再回到之前的例子中,首先通过了SubList中的add方法,再执行ArrayList中的add方法,那么这势必造成modCount在这两个类的实例中不等。因此也就会出现上文中的异常。
3.阿里规范
在阿里巴巴的jdk代码规范中也有这一条:
也就是说,SubList本身并不是创建了一个新的ArrayList。而是一个新的视图类SubList。这个类还不能强转为ArrayList。对其任何结构上的操作,都会导致底层的ArrayList被修改。
阿里开发手册还有另外一条约定:
因此我们在使用ArrayList实现的subList方法的时候要特别小心,避免踩坑。
4.正确的打开方式
如果我们需要subList的这个子集,同时又不想对原有的ArrayList造成影响,那么我们可以对这个新的subList进行拷贝。这就类似于一个CopyOnRead。
这样:
List sub = list.subList(0,3);
sub = Lists.newArrayList(sub);
或者:
List sub = list.stream().skip(0).limit(3).collect(Collectors.toList());