ArrayList扩容机制分析

ArrayList底层数据结构就是数组,用数组来存储元素。既然是用到数组,就涉及到扩容的问题。

  • size变量是实际存储元素的个数,项目代码中通常用ArrayList.size()方法来获取元素的个数。
  • 数组容量,是指存储元素的数组的长度:elementData.length

3个构造方法

java 复制代码
/**
 * 默认初始容量大小
 */
private static final int DEFAULT_CAPACITY = 10;

private static final Object[] EMPTY_ELEMENTDATA = {};

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

/**
 * 默认构造函数,使用初始容量10构造一个空列表(无参数构造)
 * 如果不往里面add元素,实际列表的容量是0
 */
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

/**
 * 带初始容量参数的构造函数。(用户自己指定容量)
 */
public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {//初始容量大于0
        //创建initialCapacity大小的数组
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {//初始容量等于0
        //创建空数组
        this.elementData = EMPTY_ELEMENTDATA;
    } else {//初始容量小于0,抛出异常
        throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity);
    }
}


/**
 * 构造包含指定collection元素的列表,这些元素利用该集合的迭代器按顺序返回
 * 如果指定的集合为null,throws NullPointerException。
 */
public ArrayList(Collection<? extends E> 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时,实际上初始化赋值的是一个空数组。当真正对数组进行添加元素操作时,才真正分配容量。即向数组中添加第一个元素时,数组容量扩为 10 (如果是调用有参构造方法创建ArrayList,debug之后看了初始化赋值的也是一个空数组,数组容量并不是传入的大小。?)

扩容入口:add()方法

java 复制代码
public boolean add(E e) {
    // 加元素之前,先调用ensureCapacityInternal方法,
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    // 这里看到ArrayList添加元素的实质就相当于为数组赋值
    elementData[size++] = e;
    return true;
}

判断是否扩容:ensureCapacityInternal()方法

上述add方法调用的ensureCapacityInternal()方法如下

java 复制代码
// 直接调用calculateCapacity和ensureExplicitCapacity方法
private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

// 根据给定的最小容量和当前数组元素来计算所需容量。
private static int calculateCapacity(Object[] elementData, int minCapacity) {
    // 满足该条件,即为初始数组为空的情况。此时所需容量取默认容量和最小容量中的最大值
    // 初始情况下,minCapacity总是1,所以首次计算的容量是10
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

// 判断当前数组容量是否足以存储上述计算出来的minCapacity个元素。
// 如果当前数组容量不够,则需要调用grow方法,进行扩容。
private void ensureExplicitCapacity(int minCapacity) {
    modCount++;

    // 如果当前数组大小,小于minCapacity,则需要进行扩容
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

判断是否扩容流程图

总结

  • 若调用无参的构造方法进行初始化,则在添加第1个元素的时候需要进行一次扩容,进入grow方法
  • 当add第2个元素时,minCapacity为2(size+1),此时elementData.length(数组容量)在添加第1个元素后扩容成10了,所以此时不会进入grow方法。同理添加第3、4、...10个元素时,都不会触发扩容。
  • 添加第11个元素的时候,minCapacity为11,比elementData.length(为10)要大,所以需要扩容

扩容的步骤:grow()方法

java 复制代码
// 核心思想是:创建一个更大的新数组,将老数组的元素复制到新数组中。

/**
 * 要分配的最大数组大小
 */
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

private void grow(int minCapacity) {
    // overflow-conscious code
    // oldCapacity为旧容量,newCapacity为新容量。
    int oldCapacity = elementData.length;
    // 将oldCapacity右移一位,相当于oldCapacity/2。
    // 计算结果为:newCapacity为oldCapacity的1.5倍。
    // ArrayList 每次扩容之后容量都会变为原来的 1.5 倍左右(oldCapacity 为偶数就是 1.5 倍,否则是 1.5 倍左右)!
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    
    // 若计算的新容量仍小于最小需要的容量,则将新容量赋值为最小需要的容量。
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
        
    // 如果新容量大于最大数组容量,则调用hugeCapacity方法比较 minCapacity 和 MAX_ARRAY_SIZE,规则如下:
    // 如果minCapacity大于最大容量,则新容量为Integer.MAX_VALUE;
    // 否则,新容量大小为 MAX_ARRAY_SIZE 即为 `Integer.MAX_VALUE - 8`
    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);
}
  • add 第 1 个元素时,oldCapacity 为 0,经比较后第一个 if 判断成立,newCapacity = minCapacity(为 10)。但是第二个 if 判断不会成立,即 newCapacity 不比 MAX_ARRAY_SIZE 大,则不会进入 hugeCapacity 方法。数组容量为 10,add 方法中 return true,size 增为 1。

  • add 第 11 个元素进入 grow 方法时,newCapacity 为 15,比 minCapacity(为 11)大,第一个 if 判断不成立。新容量没有大于数组最大 size,不会进入 hugeCapacity 方法。数组容量扩为 15,add 方法中 return true,size 增为 11。

  • 以此类推······

这里补充一点比较重要,但是容易被忽视掉的知识点:

  • Java 中的 length属性是针对数组说的,比如说你声明了一个数组,想知道这个数组的长度则用到了 length 这个属性。
  • Java 中的 length() 方法是针对字符串说的,如果想看这个字符串的长度则用到 length() 这个方法。
  • Java 中的 size() 方法是针对泛型集合说的,如果想看这个泛型有多少个元素,就调用此方法来查看。

扩容步骤流程图

参考文章

  1. JavaGuide面试题
相关推荐
盖世英雄酱581367 小时前
Java 组长年终总结:靠 AI 提效 50%,25 年搞副业只赚 4k?
后端·程序员·trae
+VX:Fegn08958 小时前
计算机毕业设计|基于springboot + vue在线音乐播放系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计
code bean8 小时前
Flask图片服务在不同网络接口下的路径解析问题及解决方案
后端·python·flask
+VX:Fegn08958 小时前
计算机毕业设计|基于springboot + vue律师咨询系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端·课程设计
努力的小郑8 小时前
2025年度总结:当我在 Cursor 里敲下 Tab 的那一刻,我知道时代变了
前端·后端·ai编程
颜淡慕潇10 小时前
深度解析官方 Spring Boot 稳定版本及 JDK 配套策略
java·后端·架构
Victor35610 小时前
Hibernate(28)Hibernate的级联操作是什么?
后端
Victor35610 小时前
Hibernate(27)Hibernate的查询策略是什么?
后端
飞鸟真人10 小时前
Redis面试常见问题详解
数据库·redis·面试
superman超哥11 小时前
Rust 内部可变性模式:突破借用规则的受控机制
开发语言·后端·rust·rust内部可变性·借用规则·受控机制