【2021-07-02】了解 ArrayList 的扩容机制吗?扩容因子是多少?为什么?

624 阅读2分钟

请移步至 【DobbyKim 的每日一题】 查看更多的题目~

答:

我们来看下 ArrayList 的 add 方法:

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

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

每次向 ArrayList 中添加一个元素时,都会先执行 ensureCapacityInternal 方法,确保 ArrayList 底层数组 elementData 的容量是够用的,如果 elementData 容量不为空,则执行 ensureExplicitCapacity 方法。

ensureExplicitCapacity 方法中进行判断:如果当前 ArrayList 的 size + 1 大于 elementData 的容量大小,则执行扩容方法 grow

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

我们看到,在 grow 方法中,声明了新容量的大小 newCapacity:

int newCapacity = oldCapacity + (oldCapacity >> 1);

其大小为旧容量(oldCapacity)的 1.5 倍,而扩容的操作实质是开辟了新的数组空间,使用 Arrays.copyOf 方法将旧数组的所有元素写入到新的数组中。

所以,ArrayList 的扩容因子为 1.5

那么,为何扩容因子被设计成 1.5 倍呢?

假定,我们每次只向 ArrayList 中添加一个元素,如果此时 ArrayList 底层数组已满就使用增长因子 k 来进行扩容。

如果给定一个大小为 n 的数组,扩容所需要的开销为

那么,插入 个元素所需的开销就是:

【2021-07-02】了解 ArrayList 的扩容机制吗?扩容因子是多少?为什么?

添加每个元素的均摊开销为:

【2021-07-02】了解 ArrayList 的扩容机制吗?扩容因子是多少?为什么?

对其求导得:

【2021-07-02】了解 ArrayList 的扩容机制吗?扩容因子是多少?为什么?

所以,当 时,,此时 有最小值。

通过上述的推导,我们得到结论,扩容因子如果被设计成 2 可能是最优的,但是这样做仍然有缺陷,使用 2 作为扩容因子的问题在于,每次扩展的新尺寸必然刚好大于之前分配的总和,也就是说,之前分配的内存空间不可能被使用。这样对缓存是不友好的,最好将扩容因子设置为

以初始容量为 4 作为示例,我们将扩容因子 k 设为 2 和 1.5 进行内存可用性的对比:

【2021-07-02】了解 ArrayList 的扩容机制吗?扩容因子是多少?为什么?

因为数组的扩容本质就是,不断开辟比原数组更大的连续内存空间,当扩容因子 k 设置为 2 时,我们发现,每次新开辟的数组都比之前分配的内存空间总和要大一些,这样导致之前的内存空间不可复用。而在 k 设置为 1.5 时,我们发现,之前的内存空间可以得到复用,至于这个数字为何是 1.5,原因在于:

int newCapacity = oldCapacity + (oldCapacity >> 1);

1.5 可以通过位运算得出,位运算可以优化计算性能。