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

时间:2022-07-22
本文章向大家介绍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<String> 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.
     *
     * <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);
    }

可以看到,这个类返回了个内部类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<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其本身是继承了AbstractList并实现了RandomAccess接口。因此在很多情况下,我们如果希望将得到的subList直接按ArrayList进行使用,则是肯定不正确的。

List<String> 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());