讲讲ArrayList的扩容和什么是溢出感知代码

落花浮王杯 提交于 2020-01-27 09:52:29

ArrayList 的扩容分为主动扩容和自动扩容两种。主动扩容就是通过调用 ArrayList 提供的 ensureCapacity() 方法来主动增加 ArrayList 实例的容量。自动扩容就是向 ArrayList 实例添加元素时,如果容量不够,ArrayList 自动扩容的过程。
无论是主动扩容还是自动扩容,最终都是通过 grow() 方法来完成扩容,我们首先来看一下 grow() 方法。

一、扩容方法 grow()

grow(int minCapacity) 方法实现的功能用一句话概括就是,如果 minCapacity 小于等于原数组的 1.5 倍,则扩容至原数组的 1.5 倍,如果 minCapacity 大于原数组的 1.5 倍,则扩容至 minCapacity。
请看源码:

    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1); // newCapacity 是 oldCapacity 的 1.5 倍
        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);
    }

其中 elementData 为 ArrayList 底层存储数据的 Object 数组,

    transient Object[] elementData; // non-private to simplify nested class access

另外还牵扯到一个常量和一个函数:

    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

我们除了要关注上面方法实现的功能外,还要关注溢出-感知代码(overflow-conscious code)。

什么是溢出感知代码?

当我们的代码需要做 a < b 这种判断时,如果我们提前感知 a、b 可能溢出,那么我们将 a < b 改成 a-b < 0,就叫做溢出感知代码。
在 a、b 发生溢出时,a-b < 0 比 a < b 拥有更好的容错性。这也是源码写 newCapacity - minCapacity < 0newCapacity - MAX_ARRAY_SIZE > 0 的原因。

我们首先来看一下什么是整型溢出:
在这里插入图片描述
如图所示,当整型变量的值为 Integer.MAX_VALUE(2147483647,-1>>>1)时,继续累加一个正的整型值,就会变成一个负数,这种情况称之为上溢。当整型变量的值为 Integer.MIN_VALUE(-2147483648) 时,继续累加一个负的整型值,就会变成一个非负数,这种情况称之为下溢

在集合扩容的过程中,只会发生上溢,所以我们只讨论上溢的情况,下溢的情况类似可得。
假设后面使用的变量都是正的整型变量,简记常量 Integer.MAX_VALUE 为 IMAX,Integer.MIN_VALUE 为 IMIN,下面分三种情况讨论 a < b 和 a-b < 0 的区别:

一、a,b 都溢出

假设 a=IMAX+a0,b=IMAX+b0a=\mathrm{IMAX}+a_0,b=\mathrm{IMAX}+b_0a0<b0a_0<b_0
其中 a0,b0a_0,b_0 分别为 a,ba,b 的溢出部分,
易知 a<ba<bab=a0b0<0a-b=a_0-b_0<0 都能返回正确结果 true。

二、a 溢出

此时 aabb 大,期望返回的结果为 false。
aa 溢出变为负数,而 bb 为正数,a<ba<b 一定返回 true,与期望不符。
假设 a=IMAX+a0a=\mathrm{IMAX}+a_0
ab=IMAX+a0ba-b=\mathrm{IMAX}+a_0-b
a0b>0a_0-b>0 时,aba-b 发生上溢,此时 ab<0a-b<0 返回 true,与期望不符。
a0b0a_0-b\le0 时,aba-b 大于 0,此时 ab<0a-b<0 返回 false,与期望相符。

可知,当 aa 溢出时,只要溢出部分 a0a_0 小于等于 bbab<0a-b<0 就能返回正确结果。

三、b 溢出

此时 aabb 小,期望返回的结果为 true。
bb 溢出变为负数,而 aa 为正数,a<ba<b 一定返回 false,与期望不符。
假设 b=IMAX+b0b=\mathrm{IMAX}+b_0
ab=IMAX1+a+1b0=IMIN+a+1b0a-b=-\mathrm{IMAX}-1+a+1-b_0=\mathrm{IMIN}+a+1-b_0
a+1b0<0a+1-b_0<0 时,aba-b 发生下溢,此时 ab<0a-b<0 返回 false,与期望不符。
a+1b00a+1-b_0\ge0 时,aba-b 小于 0,此时 ab<0a-b<0 返回 true,与期望相符。

可知,当 bb 溢出时,只要溢出部分 b0b_0 小于等于 a+1a+1ab<0a-b<0 就能返回正确结果。

从上面分类讨论的结果来看,a-b < 0 并不能解决全部的溢出情况,但在集合扩容中,a,b 如果有一个数溢出,其溢出部分不会超过另一个数,所以用 a-b < 0 刚好都能得到正确的结果。

二、主动扩容

通过调用 ensureCapacity() 方法实现主动扩容。
请看源码:

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

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

这个方法首先过滤掉了 minCapacity 为负数的情况。
其中用到的两个常量分别为:

    /**
     * Default initial capacity.
     */
    private static final int DEFAULT_CAPACITY = 10;

    /**
     * 如果 elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA,当添加第一个元素时,
     * 容量扩容到默认容量值 DEFAULT_CAPACITY(10)
     */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

接着调用 ensureExplicitCapacity() 方法:

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

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

三、自动扩容

添加元素时,自动扩容:

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

        ensureCapacityInternal(size + 1);  // Increments modCount!!
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        elementData[index] = element;
        size++;
    }
    
    public boolean addAll(Collection<? extends E> c) {
        Object[] a = c.toArray();
        int numNew = a.length;
        ensureCapacityInternal(size + numNew);  // Increments modCount
        System.arraycopy(a, 0, elementData, size, numNew);
        size += numNew;
        return numNew != 0;
    }
    
    public boolean addAll(int index, Collection<? extends E> c) {
        rangeCheckForAdd(index);

        Object[] a = c.toArray();
        int numNew = a.length;
        ensureCapacityInternal(size + numNew);  // Increments modCount

        int numMoved = size - index;
        if (numMoved > 0)
            System.arraycopy(elementData, index, elementData, index + numNew,
                             numMoved);

        System.arraycopy(a, 0, elementData, index, numNew);
        size += numNew;
        return numNew != 0;
    }

所有的添加方法都会调用 ensureCapacityInternal() 方法来扩容,

    private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }

ensureCapacityInternal() 方法首先调用 calculateCapacity() 方法,

    private static int calculateCapacity(Object[] elementData, int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }

接着和主动扩容一样,调用 ensureExplicitCapacity() 方法。

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!