【Java集合】详解ArrayList中的扩容机制

1,829 阅读7分钟

ArrayList是如何扩容的?

1. 问题引入

emmmmm,总得来说是因为自己最近想好好看一下ArrayList底层源码,好好巩固下自己的基础知识。


2. 情景还原

在研究ArrayList底层源码的时候,被其中的扩容方法困惑很久。

所以打算好好写篇博文研究下。


3. 正文

前置说明

  • ArrayList里面的size表示数组中具有多少个元素,而不是整个数组的长度!!! 比如我底层数组的长度是10,但是只有5个元素。 此时length=10 , size = 5

  • ArrayList的一些成员属性,下面讲解的时候会遇到

    // 调用无参构造方法时,ArrayList底层数组默认为{}
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    
    // 调用构造方法时,如果不指定大小的时候,那么数组初次扩容为10
    private static final int DEFAULT_CAPACITY = 10;
    
    // ArrayList里面定义的最大数组长度
    /* 
    至于为什么减8 ,详情可以参考下https://stackoverflow.com/questions/35756277/why-the-maximum-array-size-of-arraylist-is-integer-max-value-8
    对于空出的8位,目前主流解释是 :
    ①存储Headerwords;
    ②避免一些机器内存溢出,减少出错几率,所以少分配
    ③最大还是能支持到Integer.MAX_VALUE(当Integer.MAX_VALUE-8依旧无法满足需求时)
    */
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
    

3.1 什么时候进行扩容?

总结:当我们调用add方法时,会判断数组当前的lengthsize是否相等来决定是否扩容。若相等则扩容,反之不扩容。


3.2 扩容机制是什么?

代码解析:

3.2.1 首先,创建ArrayList并进行add方法

// 首先,我们来创建一个ArrayList,并进行add操作
List<Integer> list = new ArrayList<>();// 进入下面的无参构造器
list.add(1);// 进入下面的add方法

3.2.2 调用无参构造器

// ArrayList的无参构造器
public ArrayList() {
    // 即此时elementData = {}
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

3.2.3 调用add(E e)方法

因为add方法中调用了很多方法,为了方便讲解和观看,我就全部放在一个代码块里面吧,大家可以多看注释O(∩_∩)O

// 进入add(E e)方法
// ArrayList.java 496行
public boolean add(E e) {
    // 修改次数+1
    modCount++; 
    // 此时size 为 0
    add(e, elementData, size);// 跳转到483行,看下面
    // 若无异常发生,则返回true,表示添加成功
    return true; 
}

// ArrayList.java 483行
private void add(E e, Object[] elementData, int s) {
    /* 
    这里就是扩容的判断条件,判断size和length是否相等,相等则进行扩容,反之不处理。
    注意!这里的判断是在添加完之后size+1之前!!!即添加前判断是否扩容
    因为我们没有添加任何元素,所以很明显这里size和length相等 
    */
    if (s == elementData.length)
        elementData = grow();// 跳转到ArrayList.java 241行
    // 添加新元素到数组中
    elementData[s] = e;
    // size 增加 1 
    size = s + 1;
}

// ArrayList.java 241行
private Object[] grow() {
    // size+1,因为我们这里只是调用add()方法,只能添加一个元素,所以+1,如果你是添加一个集合,那情况就不是这样。
    // 即 grow(0+1),即原数组size + 添加元素的个数
    return grow(size + 1);// 跳转到ArrayList.java 236行
}

// ArrayList.java 236行
// 这里的minCapacity 就是上面的size + 1=1,表示当前数组需要的最小容量
private Object[] grow(int minCapacity) {
    return elementData = Arrays.copyOf(elementData,
                                       // newCapacity()方法就是扩容的核心方法
                                       // 跳转到ArrayList.java 254行
                                       newCapacity(minCapacity));
        				   /* 
        				   当newCapacity方法返回一个数值时,
        				   会进行Arrays.copyOf方法进行数组的复制,即创建一个新长度的数组并赋值给原数组
        				   */
}

// ArrayList.java 254行
private int newCapacity(int minCapacity) {
    // 先获取数组的length
    int oldCapacity = elementData.length;
    // newCapacity就是扩容后的长度,扩容为原数组的1.5倍,即此时 0*1.5=0
    // oldCapacity >> 1为有符号右移,即除以2
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    // 扩容后的长度和 minCapacity比较,即判断扩容后的长度和最小容量的大小关系
    // 若newCapacity ≤ minCapacity  (0 - 1 <0) 符合条件
    if (newCapacity - minCapacity <= 0) {
        // 判断数组是否为默认数组
        // 为了防止大家遗忘,这里再贴一下 DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {},DEFAULT_CAPACITY = 10;
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
            /* 
            若是,则取DEFAULT_CAPACITY 和 minCapacity中的较大者
            这里说明一下,当你调用addAll方法添加一个集合时,
            minCapacaity等于原size+集合的size,此时minCapacity有可能较大
            这里我们调用add(),只添加一个元素(即0+1),所以DEFAULT_CAPACITY肯定更大
            */
            return Math.max(DEFAULT_CAPACITY, minCapacity);// return 10 ,返回到上面的 236 行
        
        // 如果minCapacity < 0,说明发生溢出。因为int范围是 [2^31,2^31 -1] 
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        // 以上两个条件都不满足,直接返回minCapacity即可
        return minCapacity;
    }
    // 若newCapacity > minCapacity,此时进行三元运算
    /* 
    newCapacity和ArrayList里面定义的MAX_ARRAY_SIZE进行比较
    若前者 ≤ 后者,直接返回扩容后的长度 newCapacity
    反之,调用hugeCapacity(minCapacity)
    */
    return (newCapacity - MAX_ARRAY_SIZE <= 0)
        ? newCapacity
        : hugeCapacity(minCapacity);// 跳转到270行
}


// ArrayList.java 270行
private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) // 溢出则抛错
        throw new OutOfMemoryError();
    // 若minCapacity > MAX_ARRAY_SIZE,则返回Integer.MAX_VALUE
    // 否则,返回MAX_ARRAY_SIZE;
    return (minCapacity > MAX_ARRAY_SIZE)
        ? Integer.MAX_VALUE
        : MAX_ARRAY_SIZE;
}

3.2.4 调用addAll(Collection<? extends E> c) 方法

ArrayList中的这个方法是可以直接添加一个集合,我们这里着重讲一下此处的扩容机制

我们看到代码的11和12行,我们又看到了熟悉的grow方法。

public boolean addAll(Collection<? extends E> c) {
    // 将集合变成数组
    Object[] a = c.toArray();
    // 修改次数+1
    modCount++;
    // 获取想要添加的数组的长度
    int numNew = a.length;
    // 如果长度为0,则返回false
    if (numNew == 0)
        return false;
    // 声明一个数组,用于接收
    Object[] elementData;
    final int s;
    // (elementData = this.elementData).length - (s = size)) 指的是原数组中空余位置
    // 也就是说,这个if语句是在判断 想要添加的集合的长度 是否比 原数组中空余长度 大
    // 如果符合条件,说明原数组需要扩容
    if (numNew > (elementData = this.elementData).length - (s = size))
        elementData = grow(s + numNew);
    System.arraycopy(a, 0, elementData, s, numNew);
    size = s + numNew;
    return true;
}

3.3 总结

总的来说:

  • 当我们调用add方法时,会判断数组当前的lengthsize是否相等来决定是否扩容。若相等则扩容,反之不扩容。
  • 当我们调用addAll方法,会判断数组剩下的长度和想要添加的集合的长度的大小关系,若小于则进行扩容,反之不扩容。
  • 即,能放下不扩容,放不下就扩容。

扩容长度为默认为原数组的1.5倍,若不满足,则会继续进行行一系列判断,并返回一个合理的长度,

最后根据Arrays.copyOf()方法,返回一个扩容后的数组。

因为本篇文章主要讲解ArrayList的扩容机制,所以这里就不花篇幅介绍该方法。

简单来说就是调用了native方法System.arrayCopy,即底层是C去完成。

4. 测试

相信大家看了那么多文字,想必也很累了吧。

那么接下来,我们就用代码说话!

这里的getLength方法只是为了方便测试获取ArrayList底层数组的length,大家看main方法就行

输出结果为:

接着,我们试着向ArrayList添加一个元素

输出结果为:

此时,我们可以看到,数组的length的确扩容为10,且size为1

接着我们添加10个看一下

输出结果:

此时我们看到,当length和size相等时,数组并没有去扩容,因为当你第十次循环时调用add方法,此时size还是9!=10

为了验证我们代码解析时的说法,我们接下来再试着添加一个元素看看。

输出结果:

哈哈!答案是不是和你心中的一样呢?我们可以看到数组的length的确进行了1.5倍的扩容!!!

接下来我们再测试下addAll方法 我们在添加完11个元素的基础上直接添加一个长度为10的集合

输出结果:
我们可以看到数组由15扩容成了22

接下来,我们再试着由原长度11直接添加15个,看看会发生什么

输出结果:

这里我们可以看到,数组长度变成了26=11+15,这个例子可以看到,当默认扩容长度1.5倍不满足时,数组长度为minCapacity即原数组长度+添加元素的长度。

5. 写在最后

看完本篇文章,我相信你对ArrayList的扩容机制有了更多的理解。

希望大家都能一起进步呀!!!

enjoy~