浅谈ArrayList的扩容机制

源码

以下为jdk21中ArrayList部分源码

typescript 复制代码
private Object[] grow() {
    return grow(size + 1);
}
​
private Object[] grow(int minCapacity) {
    int oldCapacity = elementData.length;
    if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        int newCapacity = ArraysSupport.newLength(oldCapacity,
                minCapacity - oldCapacity, /* minimum growth */
                oldCapacity >> 1           /* preferred growth */);
        return elementData = Arrays.copyOf(elementData, newCapacity);
    } else {
        return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
    }
}

ArrayList中有两个grow方法,实现逻辑均在grow(int minCapacity)方法中,grow()方法是对grow(int minCapacity)的调用封装。

实现解析

typescript 复制代码
private Object[] grow(int minCapacity) {
    // 1.获取当前数组的长度
    int oldCapacity = elementData.length;
    if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        // 2.若数据长度>0或数组不等于默认的空数组,则计算扩容后的数数组长度。
        // 2.1扩容后长度为:minCapacity或员长度的1.5倍
        int newCapacity = ArraysSupport.newLength(oldCapacity,
                minCapacity - oldCapacity, /* minimum growth */
                oldCapacity >> 1           /* preferred growth */);
        // 3.使用Arrays.copyOf将数组元素拷贝到新数组
        return elementData = Arrays.copyOf(elementData, newCapacity);
    } else {
        // 2.若数组长度小于等于0或数组等于默认的空数组,则初始化数组,数组容量取DEFAULT_CAPACITY与minCapacity中的最大值。
        return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
    }
}

实现逻辑已注释在源码上,可自行查看,下面着重分析实现细节。

DEFAULTCAPACITY_EMPTY_ELEMENTDATA的作用

ArrayList源码中分别定义了两个成员变量EMPTY_ELEMENTDATADEFAULTCAPACITY_EMPTY_ELEMENTDATA,初始值都为空数组,两者有何区别,为什么要分别定义。

ini 复制代码
private static final Object[] EMPTY_ELEMENTDATA = {};
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

可以分别查看两个变量在何处使用来分析其作用:

csharp 复制代码
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);
    }
}
​
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

结合扩容方法可得出以下结论:

  1. EMPTY_ELEMENTDATADEFAULTCAPACITY_EMPTY_ELEMENTDATA均在ArrayList构造函数中引用,表示创建了一个元素为空的数组。
  2. 差异在于,new ArrayList<Object>(0)new ArrayList<Object>()创建list对象后,第一次往list中添加元素,扩容的逻辑不一样,举例,如使用add()方法添加元素,new ArrayList<Object>()扩容后的长度为10,而new ArrayList<Object>(0)为1(具体可分析以上扩容代码)。

为什么要分别定义(以下内容来自AI):

JDK 之所以这么做,并不是说"技术上必须要两个空数组",而是为了:

1.区分用户显式容量 = 0 和隐式默认容量的语义;

2.节省内存(懒初始化);

3.保持 API 的行为一致性(默认容量仍然是 10);

4.成本极低,用两个常量数组就能实现。

ArraysSupport.newLength的方法实现

java 复制代码
 public static int newLength(int oldLength, int minGrowth, int prefGrowth) {
    // preconditions not checked because of inlining
    // assert oldLength >= 0
    // assert minGrowth > 0
    // 1.计算所需长度:当前长度 +  Math.max(扩容所需最小长度, oldLength >> 1)
    int prefLength = oldLength + Math.max(minGrowth, prefGrowth); // might overflow
    if (0 < prefLength && prefLength <= SOFT_MAX_ARRAY_LENGTH) {
      // 2.若长度>0且小于等于Integer.MAX_VALUE - 8,直接返回
      return prefLength;
    } else {
        // put code cold in a separate method
        return hugeLength(oldLength, minGrowth);
    }
}
​
private static int hugeLength(int oldLength, int minGrowth) {
    // 1.获取所需最小容量
    int minLength = oldLength + minGrowth;
    if (minLength < 0) { // overflow
        // 2.溢出抛异常
        throw new OutOfMemoryError(
            "Required array length " + oldLength + " + " + minGrowth + " is too large");
    } else if (minLength <= SOFT_MAX_ARRAY_LENGTH) {
        // 2.小于等于最大安全长度,统一返回最大安全长度
        return SOFT_MAX_ARRAY_LENGTH;
    } else {
        // 2.直接返回minLength
        return minLength;
    }
}

为什么使用Integer.MAX_VALUE - 8作为阈值(以下来自AI):

这是为了 留出数组对象头和实现开销的空间 ,避免在某些 JVM 上出现 OutOfMemoryErrorInternalError

  • 在 HotSpot JVM 里,Java 数组在底层是一个对象,包含:

    1. 对象头 (Object Header) :存放哈希码、GC 信息、锁信息等。
    2. 数组长度字段(数组对象特有)。
    3. 数组元素数据区
  • 不同 JVM、不同平台(32 位/64 位、开启压缩指针与否),对象头大小不一样(通常 12 或 16 字节),加上对齐填充,也需要额外空间。

👉 如果直接允许数组长度达到 Integer.MAX_VALUE,在某些实现下可能因为额外的头部/对齐开销导致实际所需内存超过 2^31 - 1,从而报错。

为什么是8

-8 是 JDK 里一个 安全余量 (safety margin)

它不是严格的对象头大小(因为对象头可能是 12、16、24 字节),而是一个经验性的保守值。

这样即使在不同 JVM 实现或未来调整下,new Object[Integer.MAX_VALUE - 8] 依然大概率能成功,而不会立刻抛错。

相关推荐
珹洺3 分钟前
Java-Spring入门指南(十五)SpringMVC注解开发
java·spring·microsoft
小满、3 分钟前
什么是Maven?关于 Maven 的坐标、依赖管理与 Web 项目构建
java·maven
半旧夜夏33 分钟前
【设计模式】核心设计模式实战
java·spring boot·设计模式
半旧夜夏1 小时前
【Spring】AOP的核心原理配方
java·spring
qiuiuiu4132 小时前
CPrimer Plus第十六章C预处理器和C库总结2-qsort函数
java·c语言·算法
IT学长编程2 小时前
计算机毕设选题 基于SpringBoot的书店管理系统的设计与实现 网上书店系统 前后端分离 Java毕设项目 毕业设计选题 【附源码+文档报告+安装调试】
java·spring boot·毕业设计·课程设计·前后端分离·网上书店系统·书店管理系统
张较瘦_2 小时前
应用型本科计算机类专业毕业设计与论文选题指南
java·开发语言·课程设计
IT学长编程2 小时前
计算机毕设选题 基于SpringBoot的房产租赁管理系统 房屋租赁系统 前后端分离 Java毕设项目 毕业设计选题 【附源码+文档报告+安装调试】
java·spring boot·毕业设计·课程设计·房屋租赁系统·房产租赁系统·文档报告