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面试题
相关推荐
PAK向日葵3 小时前
【算法导论】PDD 0817笔试题题解
算法·面试
uzong5 小时前
技术故障复盘模版
后端
GetcharZp5 小时前
基于 Dify + 通义千问的多模态大模型 搭建发票识别 Agent
后端·llm·agent
桦说编程6 小时前
Java 中如何创建不可变类型
java·后端·函数式编程
IT毕设实战小研6 小时前
基于Spring Boot 4s店车辆管理系统 租车管理系统 停车位管理系统 智慧车辆管理系统
java·开发语言·spring boot·后端·spring·毕业设计·课程设计
wyiyiyi6 小时前
【Web后端】Django、flask及其场景——以构建系统原型为例
前端·数据库·后端·python·django·flask
阿华的代码王国7 小时前
【Android】RecyclerView复用CheckBox的异常状态
android·xml·java·前端·后端
Jimmy7 小时前
AI 代理是什么,其有助于我们实现更智能编程
前端·后端·ai编程
AntBlack8 小时前
不当韭菜V1.1 :增强能力 ,辅助构建自己的交易规则
后端·python·pyqt
bobz9658 小时前
pip install 已经不再安全
后端