美文网首页Java
ArrayList源码分析(基于jdk1.8)(二):subLi

ArrayList源码分析(基于jdk1.8)(二):subLi

作者: 冬天里的懒喵 | 来源:发表于2020-08-07 14:56 被阅读0次

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


    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)(二):subLi

        本文链接:https://www.haomeiwen.com/subject/xrvfdktx.html