ArrayList源码分析(基于jdk1.8)(二):subList陷阱

在上文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代码规范中也有这一条:


ArrayList源码分析(基于jdk1.8)(二):subList陷阱_第1张图片
image.png

也就是说,SubList本身并不是创建了一个新的ArrayList。而是一个新的视图类SubList。这个类还不能强转为ArrayList。对其任何结构上的操作,都会导致底层的ArrayList被修改。
阿里开发手册还有另外一条约定:


image.png

因此我们在使用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());

你可能感兴趣的:(ArrayList源码分析(基于jdk1.8)(二):subList陷阱)