温馨提示: 豌豆仅提供国内节点,不提供境外节点,不能用于任何非法用途,不能访问境外网站及跨境联网。

免费领取1万IP!

关于ArrayList源码的一些自我理解以及解析(三):batchRemove方法的理解

发布时间:

之前看到了removeAll和retainAll方法里面都调用了batchRemove方法,只不过传递的参数不同,于是决定好好的看一看这个方法。

private boolean batchRemove(Collection<?> c, boolean complement) {
        final Object[] elementData = this.elementData;
        int r = 0, w = 0;
        boolean modified = false;
        try {
            for (; r < size; r++)
                if (c.contains(elementData[r]) == complement)
                    elementData[w++] = elementData[r];
        } finally {
            // Preserve behavioral compatibility with AbstractCollection,
            // even if c.contains() throws.
            if (r != size) {
                System.arraycopy(elementData, r,
                                 elementData, w,
                                 size - r);
                w += size - r;
            }
            if (w != size) {
                // clear to let GC do its work
                for (int i = w; i < size; i++)
                    elementData[i] = null;
                modCount += size - w;
                size = w;
                modified = true;
            }
        }
        return modified;
    }

乍一看觉得还是可以理解的,但是看到第8行的时候就一脸的懵逼了,”elementData[w++]=elementData[r]“名字都一样的,你让怎么知道你的意思?于是我尝试从两个int类型变量名字入手。变量r应该是read的意思,意思就是读取,这种解释也符合了为什么for循环里面不断变化的是r。变量w应该就是write的意思,写入的意思,这也解释了为什么当判断条件成立的时候,w才会发生变化。

有了这层理解就好了,既然r是读取,w是写入,那么”elementData[w++]=elementData[r]“前面的这个elementData是方法自己定义的那个变量,后面那个elementData是ArrayList自己定义的变量。也就是说,前面这个elementData在出了batchRemove方法后就没有用了,于是我将代码自己理解了一下:

    private boolean batchRemove(Collection<?> c, boolean complement) {
        final Object[] BRelementData = this.elementData;
        int r = 0, w = 0;
        boolean modified = false;
        try {
            for (; r < size; r++)
                if (c.contains(BRelementData[r]) == complement)
                    elementData[w++] = BRelementData[r];
        } finally {
            // Preserve behavioral compatibility with AbstractCollection,
            // even if c.contains() throws.
            if (r != size) {
                System.arraycopy(BRelementData, r,
                                 elementData, w,
                                 size - r);
                w += size - r;
            }
            if (w != size) {
                // clear to let GC do its work
                for (int i = w; i < size; i++)
                    elementData[i] = null;
                modCount += size - w;
                size = w;
                modified = true;
            }
        }
        return modified;
    }

这样一来就很容易理解了,根据”c.contains(elementData[r]) == complement“这个的结果,BRelementData里面存储的就是想要的结果了。而这段for循环却被try包裹着,后面还跟上了一个finally结构。finally的作用是,try里面的代码执行完毕后必定执行finally里面的代码。而这样做给的理由在注释中也说明了,是为了保持和AbstractCollection的兼容性。这么说也不是很懂它的意思,还是看里面的代码比较实在一点。

if (r != size) {
                System.arraycopy(BRelementData, r,
                                 elementData, w,
                                 size - r);
                w += size - r;
            }

首先,是判断r是否会等于size。如果说前面的代码不停止的话,那么最后r会将ArrayList里面的元素全部遍历完毕,最后r的值会等于size的值,并且跳出前面那个for循环。既然前面那个for循环执行完毕之后,r一定会等于size的,那为什么这里就要进行这样的判断呢?也就是说,这是在避免某种情况,避免什么样的情况,可以从代码中看得出来。

我们先让它能执行到if里面的语句,假设此时ArrayList里面的size是10,而我们的r=3,看看会发生什么。同样是熟悉的System.arraycopy方法,先将batchRemove方法自己的BRelementData数组10个元素从r=3开始进行复制,然后添加进ArrayList自己的elementData数组中,并且从w开始的位置加入,加入的长度是size-r也就是7个长度。

从r=3开始复制,前面的不复制是因为通过for循环已经遍历过了,所以复制的是那些没有遍历过的数。而后续添加的位置是从w开始,也就是说,没有遍历过的BRelementData的数据被添加到了经历过”if (c.contains(elementData[r]) == complement)“判断过后的数的后面。随后w的长度也增加了size-r个数。

if (w != size) {
                // clear to let GC do its work
                for (int i = w; i < size; i++)
                    elementData[i] = null;
                modCount += size - w;
                size = w;
                modified = true;
            }

前面的第一个if看完了,现在来看看关于w的if条件。我们先看看if里面做了什么样的事情,一个for循环,从w位置开始,一直遍历到size最后的位置,将w开始的元素以及之后的元素都设置成为null,然后重新设置size的长度为w,将后面所有的null都交给GC垃圾回收机制去处理。随后更改modified的值,表示ArrayList里面的数据确实发生了改动。

此时我们就知道了,finally里面的代码块是暂存作用,为什么只有(r!=size)的时候才去执行里面的代码块?明明try代码块里面的r最后肯定会是r=size才会跳出for循环的。那么为什么后面也要设置一个(w!=size)的判定呢?我们假设前面的直接走完了,并没有走(r!=size)里面的代码,此时有两种情况,刚好w=size,没有执行(w!=size)里面的代码,此时modified=false,我们就知道,ArrayList里面的数据并没有发生改动。另外一种情况,执行了(w!=size)里面的代码,最后size=w。

    /**
     * Save the state of the <tt>ArrayList</tt> instance to a stream (that
     * is, serialize it).
     *
     * @serialData The length of the array backing the <tt>ArrayList</tt>
     *             instance is emitted (int), followed by all of its elements
     *             (each an <tt>Object</tt>) in the proper order.
     */
    private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException{
        // Write out element count, and any hidden stuff
        int expectedModCount = modCount;
        s.defaultWriteObject();
        // Write out size as capacity for behavioural compatibility with clone()
        s.writeInt(size);
        // Write out all elements in the proper order.
        for (int i=0; i<size; i++) {
            s.writeObject(elementData[i]);
        }
        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }
    }

writeObject方法,传递的参数是一个ObjectOutputStream对象,这是一个写入流,在注释中也对这个方法进行了解释,将ArrayList的状态保存进流里面。此时,我们多次看到过的modCount就起到作用了,先定义了一个expectedModCount用来保存它。然后执行默认的序列化操作,然后将大小写入进去,然后通过for循环把所有元素按照正确的顺序写入进去。

每当ArrayList里面的内容被改动过的时候,modCount都会增加,if (modCount != expectedModCount)的判断是为了确保,当把ArrayList里面的数据写入流里面的时候是最新数据。如果在写入流的过程中,数据发生了变化,那么这个数据就不是最新的数据,那么就抛出错误ConcurrentModificationException()。

    /**
     * Reconstitute the <tt>ArrayList</tt> instance from a stream (that is,
     * deserialize it).
     */
    private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        elementData = EMPTY_ELEMENTDATA;
        // Read in size, and any hidden stuff
        s.defaultReadObject();
        // Read in capacity
        s.readInt(); // ignored
        if (size > 0) {
            // be like clone(), allocate array based upon size not capacity
            ensureCapacityInternal(size);
            Object[] a = elementData;
            // Read in all elements in the proper order.
            for (int i=0; i<size; i++) {
                a[i] = s.readObject();
            }
        }
    }

既然有写入,那么肯定就有读取。readObject方法和writeObject方法是在ArrayList里面的数据序列化的时候才会调用得到,既然是从流中读取出数据,那么就和writeObject差不多的操作,先将ArrayList里面的数据清空成为定义好的EMPTY_ELEMENTDATA空数组。然后是默认的序列化读取操作,读取里面的int,然后判断里面的大小,随后扩容,然后遍历将元素都取出来。

虽然很纳闷这这个方法,但我的理解是,for循环中存在着遍历器,即使你看起来所有的a[i]=s.readObject(),但实际上,是按照顺序一一对应读取出来的,for循环里面应该有遍历器去呼应readObject的动作,达到进去的时候是什么顺序,读出来的就是什么顺序。

关于OutputStream和InputStream的记忆有一个小技巧。可以想象成寄快递和收快递,OutputStream就像是寄快递,你把需要传递的东西打包好,然后写各种信息就OK了,这就是写入流。InputStream就像是收快递,你收到一样东西,你肯定先看下是不是你的,读包裹上面的字,然后拆开包裹取出里面的东西,这就是读出流。一个写入数据和读取数据就这样记住了。

 

以上内容来自于网络,如有侵权联系即删除

相关文章


Codeforces Round #538 (Div. 2)(solve6/6) uva11389公车早晚班(贪心详解) 在Darknet环境下训练MS COCO 2017数据集(目标检测)(YOLOv3) linux中的ftp服务 yum安装varnish Python中random.sample()的替代方案 C语言中的取绝对值函数 Java学习笔记——Map集合

上一篇:okhttp_utils的基本使用
下一篇:第一个Cocos程序
注册
联系我们
渠道合作
16522444463
大客户合作
16522444463
QQ群
qq