ArraylList的扩容机制和使用ensureCapacity()方法提高性能
阅读原文时间:2021年04月21日阅读:1

ArrayList的扩容规则是变成原来最大容量的1.5倍+1

具体为什么,现在看一下源码:

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

ensureCapacityInternal是判断是否要扩容的方法,下面是源码:

private void ensureCapacityInternal(int minCapacity) {
        if (elementData == EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        ensureExplicitCapacity(minCapacity);
    }



 private transient Object[] elementData;

elementData是存放元素的对象数组

 private static final Object[] EMPTY_ELEMENTDATA = {};

EMPTY_ELEMENTDATA是空数组,表示现在ArrayList是空的

ensureCapacityInternal中首先是判断现在的ArrayList是不是空的,如果是空的,minCapacity就取默认的容量和传入的参数minCapacity中的大值

然后调用ensureExplicitCapacity方法,

  private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

modCount是fail fast机制,在jdk1.6之前都是有volatile来修饰的,尽可能的让并发访问非安全的集合对象时尽快的失败抛出异常,让程序员修改代码。

在jdk1.7中去掉了volatile修饰,因为感觉没有必要为非线程安全集合浪费效率,在jdk1.5开始就提供了线程安全的集合类,在多线程环境下就应该使用线程安全的集合。

接着看,如果minCapacity的值大于add数据之前的大小,就调用grow方法,进行扩容,否则什么也不做。

下面是具体看出来扩容机制的大小增长规则了:

 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);
    }

注意:这里传过来的minCapcatiy的值是size+1,能够实现grow方法调用就肯定是(size+1)>elementData.length的情况,所以size就是初始最大容量或上一次扩容后达到的最大容量,所以才会进行扩容。

newCapacity=oldCapacity+(oldCapacity>>1),这里就是扩容大小确定的地方,相当于新的最大容量是 size+1+size/2 相当于原来的1.5倍然后加1

==============================================================================================================================

这样每次size达到容量后,再插入数据就会造成扩容,默认的容量大小是10,如果我们现在连续插入17的对象,就会扩容两次,第一次是在插入第11个对象时,第二次是在插入第17个对象时扩容,这样会频繁进行数组的拷贝,效率影响很大。那么我们在插入数据数量已知的情况,可以调用ensureCapacity方法。

下面是方法源码:

public void ensureCapacity(int minCapacity) {
        int minExpand = (elementData != EMPTY_ELEMENTDATA)
            // any size if real element table
            ? 0
            // larger than default for empty table. It's already supposed to be
            // at default size.
            : DEFAULT_CAPACITY;

        if (minCapacity > minExpand) {
            ensureExplicitCapacity(minCapacity);
        }
    }

为什么要先判断先判断elementData!=EMPTY_ELEMENTDATA呢?现在看一下ArrayList的无参构造函数

 public ArrayList() {
        super();
        this.elementData = EMPTY_ELEMENTDATA;
    }

这里看出来ArrayList在没有指定初始化容量的时候elementData就是一个空对象数组,没有任何对象元素,也就是容量为0,没有分配空间。

可以再看一下add方法中,调用的ensureCapacityInternal方法中判断elementData == EMPTY_ELEMENTDATA 是否为空,如果为空,minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);,minCapacity设置为其中的大值,在默认是空的时候,初次调用add方法传入的参数是1,也就是这里的minCapacity就是1,现在minCapcity变成了DEFAULT_CAPACITY 也就是10,下面会调用grow方法,然后执行 elementData = Arrays.copyOf(elementData, 10);下面注意拷贝方法:

 public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
        T[] copy = ((Object)newType == (Object)Object[].class)
            ? (T[]) new Object[newLength]
            : (T[]) Array.newInstance(newType.getComponentType(), newLength);
        System.arraycopy(original, 0, copy, 0,
                         Math.min(original.length, newLength));
        return copy;
    }

执行了第一条语句后,copy=new Objecet[10]了,返回的结果数组大小就是10了, ArrayList相当于在没指定initialCapacity时就是会使用延迟分配对象数组空间,当第一次插入元素时才分配10个对象空间。

上面分析过程也包含了扩容的步骤了。

当我们已经确定了要插入的对象的数目(并不是在创建ArrayList之前就知道有多少对象要插入的情况),就应该首先调用ensureCapacity来一次性扩容到可以容得下要插入的对象个数,这样就避免的多次进行数组拷贝的情况,提高了效率,算是优化吧
当然,我们在创建ArrayList之前就知道有多少对象要插入就使用有参构造。