栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

从源码层面理解ArrayList 扩容策略「详解」

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

从源码层面理解ArrayList 扩容策略「详解」

ArrayList 在我们日常开发中用到的非常多,我们知道 ArrayList 内部是通过 Object 数组实现的,而数组的长度一经定义,就无法更改了。

那么问题就来了,ArrayList 是如何实现扩容的呢?

我们先来看看 ArrayList 类中有哪些成员变量。

ArrayList 的成员变量
private static final int DEFAULT_CAPACITY = 10;private static final Object[] EMPTY_ELEMENTDATA = {};private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};transient Object[] elementData; // non-private to simplify nested class accessprivate int size;复制代码

问题一:ArrayList 中的size 和 capacity 怎么理解?

size 用于记录 ArrayList 实例中 elementData 数组中元素的个数,capacity 是elementData 数组的长度(包括已使用的数组空间和未使用的数组空间)。如果被 ArrayList 看作一个喝水的杯子的话,capacity 就是杯子的容积,也就是代表了杯子能装多少水,size 就是杯子已经装的水的体积。杯子可能装满了水也可能没装满。

要想使用一个类,首先要创建这个类的实例,那么接下来我们看看 ArrayList 有哪些构造方法,这是我们比较关心的。

ArrayList 的构造方法

1、 无参构造方法

注释上说,构造一个初始容量为10的空列表。实际上,Java8 中使用了延迟初始化,使用无参构造方法,并不会马上创建长度为 10 的数组,而是在调用 add 方法添加第一个元素的时候才对 elementData 数组进行初始化(后面会看到)。

public ArrayList() {    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}复制代码

2、指定初始容量的构造方法

传入初始容量 initialCapacity,如果初始容量大于 0,那么直接创建一个指定大小的 Object 数组;如果初始容量等于0,elementData 指向共享的空数组实例 EMPTY_ELEMENTDATA。如果初始容量小于0,抛出 IllegalArgumentException 异常。

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);
    }
}复制代码

3、指定初始集合的构造方法

public ArrayList(Collection c) {
    elementData = c.toArray();    if ((size = elementData.length) != 0) {        // c.toArray might (incorrectly) not return Object[] (see 6260652)
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {        // replace with empty array.
        this.elementData = EMPTY_ELEMENTDATA;
    }
}复制代码

问题二:ArrayList 源码中为何定义两个 Object 数组呢?EMPTY_ELEMENTDATA 和 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 各有什么用处?

从以上源码中可以看出,这两个常量都是空 Object 数组的引用,都表示 ArrayList 实例的空状态,即 elementData 数组中没有元素。EMPTY_ELEMENTDATA 是使用指定初始容量的构造方法 ArrayList(int initialCapacity)(初始容量大小为0) 和 指定初始集合的构造方法 ArrayList(Collection c)(初始集合大小为0) 时使用。DEFAULTCAPACITY_EMPTY_ELEMENTDATA 是使用无参构造方法时使用的。

构造方法也有了,接下来我们看看如何向 ArrayList 容器中添加一个元素。

添加元素
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;    return true;
}复制代码

add 方法向 ArrayList 中添加1个元素,为了确保 ArrayList 内部数组容量,add 方法内部首先调用 ensureCapacityInternal 方法,入参 minCapacity 为 ArrayList 包含的实际元素个数 size + 1。

private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}复制代码

ensureCapacityInternal 内部调用 calculateCapacity 方法来计算容量,如果 ArrayList 是通过无参构造方法进行创建的,那么满足下面 if 条件(elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA),如果是添加第一个元素,则minCapacity 为 1,则数组扩容到 DEFAULT_CAPACITY 大小为10,这也对应了无参构造方法的注释 Constructs an empty list with an initial capacity of ten 。

private static int calculateCapacity(Object[] elementData, int minCapacity) {    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {        // 如果是空ArrayList,则容量为 DEFAULT_CAPACITY 和 minCapacity 的最大值
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }    return minCapacity;
}复制代码

笔者当时读到 Math.max(DEFAULT_CAPACITY, minCapacity); 这行代码的时候有点小小的困惑,既然elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA 说明当前的 ArrayList 是空的,那么直接返回DEFAULT_CAPACITY 值不就行了么,为啥还要比较呢。直到后来发现了 addAll(Collection c) 这个方法,addAll 方法可以一次向 ArrayList 中添加多个元素,新增加的元素个数可能大于 DEFAULT_CAPACITY ,为了减少扩容次数,应该取 DEFAULT_CAPACITY 和 minCapacity 的最大值。

minCapacity 等于 ArrayList 当前实际元素个数size + 新增的元素个数,minCapacity是扩容后 Object 数组的最小长度。

ensureExplicitCapacity 方法确保 ArrayList 有足够的容量存放新的元素。

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}复制代码

容量不够的话,会调用 grow 方法 进行扩容操作。

扩容操作
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;private void grow(int minCapacity) {    // overflow-conscious code
    int oldCapacity = elementData.length;    // 新容量扩大到原容量的1.5倍
    int newCapacity = oldCapacity + (oldCapacity >> 1);    if (newCapacity - minCapacity < 0)        // 如果新容量还是比所需的最小容量小,则让新容量等于所需的最小容量
        newCapacity = minCapacity;    if (newCapacity - MAX_ARRAY_SIZE > 0)        // 如果新容量超过了Integer.MAX_VALUE - 8,继续计算
        newCapacity = hugeCapacity(minCapacity);    // minCapacity is usually close to size, so this is a win:
    // 所需的最小容量minCapacity 接近size
    elementData = Arrays.copyOf(elementData, newCapacity);
}复制代码

扩容计算,int newCapacity = oldCapacity + (oldCapacity >> 1); oldCapacity 是ArrayList 内部数组长度,oldCapacity >> 1 是位运算的右移操作,右移一位相当于除以2,新的容量 newCapacity 为之前容量的1.5倍。

elementData = Arrays.copyOf(elementData, newCapacity); 对 elementData 数组进行扩容。

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

问题三:ArrayList 扩容每次都是原容量的1.5倍吗?

从源码中可以看出,当使用无参构造方法创建一个 ArrayList 实例,调用 add 方法添加第一个元素的时候,calculateCapacity 方法返回的是默认初始容量 DEFAULT_CAPACITY 大小为10;当使用指定初始容量创建ArrayList 实例,调用 addAll 方法添加多个元素的时候,原容量的1.5倍也无法存放元素的时候,会创建一个更大(不会超过 Integer.MAX_VALUE)的数组来存放元素。

问题四:ArrayList 的 add 操作如何优化?

扩容需要移动数据,非常影响性能。那么优化的重点就是尽量避免 ArrayList 内部进行内部扩容。对于add 操作,如果添加的元素个数已知,最好使用指定初始容量的构造方法创建 ArrayList 实例或者在添加元素之前执行ensureCapacity 方法确保有足够的容量来存放add 操作的元素。


作者:Geek_ymv


转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/238016.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号