• ArrayList源码中的两个值得注意的问题


    1、“拖泥带水”的删除

    测试代码:

    package com.demo;
    import java.util.ArrayList;
    public class TestArrayList {
        public static void main(String[] args) {
            // TODO Auto-generated method stub
            ArrayList<String> arylist = new ArrayList<String>();
            arylist.add("aa");
            arylist.add("bb");
            arylist.add("bb");
            arylist.add("cc");
    //        arylist.add("aa");
    //        arylist.add("bb");
    //        arylist.add("cc");
    //        arylist.add("bb");
            String target = "bb";
            for (int i = 0; i < arylist.size(); i++) {
                if (arylist.get(i).equals(target)) {
                    arylist.remove(i);
                }
            }
            for(String value:arylist){
                System.out.println(value);
            }
        }
    }

    这段代码的本意在于删除动态数组arylist中元素等于target的元素,通过遍历数组的每一项,当equals判定相等就删除之。如果数组中的所有元素唯一,不会存在任何问题,对应的元素得到正确的删除;如果数组中元素有重复,那么你可能理所当然地认为所有重复元素也应该全部被删除,因为似乎遍历到了每项元素。但实际结果有可能不是你想的样子。运行上述代码得到输出:

    aa
    bb
    cc

    有一个元素"bb"应该被删除但是未被删除。

    但如果在调整一下动态数组中重复元素“bb”的位置(如注释的代码)为:{"aa","bb","cc","bb"},其结果是两个“bb”全部被删除。

    这里的重点在于remove()方法的实现。查看其源码:

        public E remove(int index) {
            rangeCheck(index);
    
            modCount++;
            E oldValue = elementData(index);
    
            int numMoved = size - index - 1;
            if (numMoved > 0)
                System.arraycopy(elementData, index+1, elementData, index,
                                 numMoved);
            elementData[--size] = null; // clear to let GC do its work
    
            return oldValue;
        }

    从以上删除的处理逻辑可以看出,删除操作主要是调用了System.arraycopy()方法,在System类中该方法的定义为:

        public static native void arraycopy(Object src,  int  srcPos,
                                            Object dest, int destPos,
                                            int length);

    该方法的功能是将源数组src从srcPos位置开始,复制数量为length的元素到目标数组dest中,其中目标数组从destPos位置开始往后填充。

    由此可知,remove方法中删除操作的处理办法是将elementData数组从index+1位置开始其后的numMoved各元素原地复制到index处。简单点说就是将index+1位置开始的元素依次往前挪动一个位置,因为index位置的元素反正要被删除,就直接覆盖它,最后在处理elementData最末尾无用的元素和更新size。

    这样处理的问题在于,如果arylist中位置为i和i+1的元素重复且刚好是需要删除的元素,那么由于一轮删除之后,第二个元素占据第一个元素的位置,但是i++自增了,第二个元素被跳过了,未被遍历到,删除实际上是不完整的。实际上只要待删除元素存在相邻的情况都会出现这种“拖泥带水”不完整的删除。

    remove(Object o)、fastRemove(int index)都是基于类似的实现方式。

    正确的写法可以在必要的时候手动修正计数器i的值,但是这种实现实际上不是很优雅:

            for (int i = 0; i < arylist.size(); i++) {
                if (arylist.get(i).equals(target)) {
                    arylist.remove(i);
                    i--;
                }
            }

    更好的实现是反向遍历删除:

            for (int i = arylist.size()-1; i >= 0; i--) {
                if (arylist.get(i).equals(target)) {
                    arylist.remove(i);
                }
            }

    2、不一样的初始化

    ArrayList是基于对象数组实现的。

    在版本JDK1.8中

    其中几个常用的成员变量有:

    默认的初始化容量:DEFAULT_CAPACITY 

    空的对象数组变量两个:EMPTY_ELEMENTDATA和DEFAULTCAPACITY_EMPTY_ELEMENTDATA

    真正存储元素的对象数组变量:elementData

    大小:size

    /**
         * Default initial capacity.
         */
        private static final int DEFAULT_CAPACITY = 10;
        private static final Object[] EMPTY_ELEMENTDATA = {};
        /**
         * Shared empty array instance used for default sized empty instances. We
         * distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
         * first element is added.
         */
        private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    
        transient Object[] elementData; // non-private to simplify nested class access
    
        private int size;

    源码的实现者特地用两个空的对象数组来区别有参和无参实例化,以满足不同条件下的使用。

    对应的构造器如下:

        public ArrayList(int initialCapacity) {
            if (initialCapacity > 0) {
                this.elementData = new Object[initialCapacity];
            } else if (initialCapacity == 0) {
                this.elementData = EMPTY_ELEMENTDATA;
            } else {
                throw new IllegalArgumentException("Illegal Capacity: "+
                                                   initialCapacity);
            }
        }
    
        /**
         * Constructs an empty list with an initial capacity of ten.
         */
        public ArrayList() {
            this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
        }

    因此,通过如下两种方式实例化后,对象数组实际上都是一个空数组,但是是不同的空数组。

            ArrayList<String> list = new ArrayList<String>(0);
            ArrayList<String> list1 = new ArrayList<String>();

    第一种实例化得到的空数组是EMPTY_ELEMENTDATA,第二种实例化得到的空数组是DEFAULTCAPACITY_EMPTY_ELEMENTDATA。

    但是随后的第一次add元素后就会有所区别。

        public boolean add(E e) {
            ensureCapacityInternal(size + 1);  // Increments modCount!!
            elementData[size++] = e;
            return true;
        }

    add()方法调用ensureCapacityInternal()方法。

        private void ensureCapacityInternal(int minCapacity) {
            if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
                minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
            }
    
            ensureExplicitCapacity(minCapacity);
        }
    
        private void ensureExplicitCapacity(int minCapacity) {
            modCount++;
    
            // overflow-conscious code
            if (minCapacity - elementData.length > 0)
                grow(minCapacity);
        }
        private void grow(int minCapacity) {
            // overflow-conscious code
            int oldCapacity = elementData.length;
            int newCapacity = oldCapacity + (oldCapacity >> 1);
            if (newCapacity - minCapacity < 0)
                newCapacity = minCapacity;
            if (newCapacity - MAX_ARRAY_SIZE > 0)
                newCapacity = hugeCapacity(minCapacity);
            // minCapacity is usually close to size, so this is a win:
            elementData = Arrays.copyOf(elementData, newCapacity);
        }

    ensureCapacityInternal()方法中特地有区别的对待了两个同为空的对象数组:

    如果对象数组是DEFAULTCAPACITY_EMPTY_ELEMENTDATA,那么第一次add元素时分配的容量直接从10起步,随后一直到size为10为止,add元素不会在调用grow()方法。

    如果对象数组是EMPTY_ELEMENTDATA,那么第一次add元素时分配的容量是1,并且随后的几次add都会比较频繁的调用grow(),这样的增幅视乎太小了,但是仅限于前面几次add操作,到后面阶段于第一种方式基本保持一直的步调。

    为何要在这里对如此细微的细节给予差别对待?我其实也不是很懂,但是大体上,猜测作者是想传递这样一个实现意图:

    如果程序员用ArrayList<String> list = new ArrayList<String>(0);来实例化,那么他应该是很明确的需要空的动态数组而不需要存储任何元素,即使需要存储元素,也是一两个很少量的元素,不会频繁的add,因此不应该在第一次add操作时分配10的容量,很可能造成浪费。

    如果程序员 ArrayList<String> list1 = new ArrayList<String>();来实例化,那么他是需要这个动态数组来存放元素的,因此应该在第一次add是为其分配合适的容量10。

    完结~~~

  • 相关阅读:
    java的-D命令行参数 mvn -D参数
    storm1.1运行时问题
    shell 日期转换
    storm单机运行与集群运行问题
    [log4j]Slf4j的包冲突
    搭建Spark源码研读和代码调试的开发环境
    Centos7配置
    mvn本地执行java程序
    HDP和HDF
    Django——如何在Django模板中注入全局变量?——part1
  • 原文地址:https://www.cnblogs.com/qcblog/p/7752135.html
Copyright © 2020-2023  润新知