ArrayList 源码扩容机制全解析

一、核心设计思想

  1. 动态数组本质

    • 底层通过 Object[] elementData 数组存储数据

    • 默认构造空数组

      java 复制代码
      public ArrayList() {
          this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; // 共享空数组
      }
  2. 扩容目标

    • 空间换时间:以 1.5 倍系数扩容,平衡内存占用与扩容频率
    • 自动扩容:无需手动管理容量,但需注意性能损耗

二、触发扩容的精确时刻

操作类型 触发条件 示例场景
add(E e) size == elementData.length 添加第 11 个元素到默认容量 10 的列表
addAll(Collection) size + 新增元素数 > elementData.length 批量添加 20 个元素到容量 15 的列表
ensureCapacity() 显式设置的容量 > 当前容量 提前扩容到 1000 以避免后续多次扩容

三、JDK11 源码级扩容流程

1. 入口方法调用链
java 复制代码
add() → add(e, elementData, size) → grow() → grow(int minCapacity)
2. 核心源码解析
java 复制代码
// JDK11
private Object[] grow(int minCapacity) {
    return elementData = Arrays.copyOf(elementData, newCapacity(minCapacity));
}

private int newCapacity(int minCapacity) {
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1); // 1.5倍计算
    
    // 处理特殊场景
    if (newCapacity - minCapacity <= 0) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { // 首次扩容
            return Math.max(DEFAULT_CAPACITY, minCapacity); // 默认10
        }
        if (minCapacity < 0) throw new OutOfMemoryError(); // 溢出检查
        return minCapacity;
    }
    
    // 处理最大容量限制
    return (newCapacity - MAX_ARRAY_SIZE <= 0) ? 
           newCapacity : hugeCapacity(minCapacity);
}

private static int hugeCapacity(int minCapacity) {
    return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;
}
3. 扩容步骤详解
  1. 容量计算

    • 基础扩容新容量 = 旧容量 × 1.5(位运算优化为 oldCapacity + (oldCapacity >> 1)

    • 特殊情况

      • 首次扩容:取 max(10, 需求容量)
      • 扩容不足时:直接使用需求容量
      • 超大容量:最大为 Integer.MAX_VALUE
  2. 数据迁移

    • 通过 Arrays.copyOf() 创建新数组
    • 底层调用本地方法 System.arraycopy() 实现高效复制

四、扩容性能特征

操作类型 时间复杂度 说明
普通添加 O(1) 无需扩容时的常规操作
扩容时的添加 O(n) 需复制 n 个元素到新数组
批量添加(addAll) O(n+m) n=原数组长度,m=新增元素数量

性能对比测试(百万级元素):

java 复制代码
// 测试代码片段
public class ArrayListTest {
    public static void main(String[] args) {
        int size = 1000000;
        
        // 测试默认构造
        long t1 = System.nanoTime();
        List<Integer> defaultList = new ArrayList<>();
        for (int i = 0; i < size; i++) {
            defaultList.add(i);
        }
        System.out.println("默认构造耗时:" + (System.nanoTime()-t1)/1000000 + "ms");

        // 测试预设容量
        long t2 = System.nanoTime();
        List<Integer> preSizedList = new ArrayList<>(size);
        for (int i = 0; i < size; i++) {
            preSizedList.add(i);
        }
        System.out.println("预设容量耗时:" + (System.nanoTime()-t2)/1000000 + "ms");
    }
}
初始容量 耗时(100万次 add)
默认 10 35 ms
1,000,000 12 ms

五、扩容过程推演

场景示例:默认构造连续添加元素

java 复制代码
List<String> list = new ArrayList<>();
for (int i=0; i<16; i++) list.add("item");
  1. 首次扩容(添加第1个元素)

    • 空数组 → 扩容到 10
  2. 常规扩容

    添加元素序号 触发扩容时容量 新容量计算
    10 10 → 15 10 + (10>>1) = 15
    16 15 → 22 15 + (15>>1) = 22
    23 22 → 33 22 + (22>>1) = 33

六、设计哲学与工程权衡

  1. 1.5 倍系数的数学原理

    • 分摊分析:经过 n 次插入操作,总时间复杂度为 O(n)
    • 空间利用率:每次扩容后剩余空间逐渐增大,减少扩容频率

七、开发最佳实践

  1. 容量预设

    java 复制代码
    // 已知要存储 50000 个用户
    List<User> users = new ArrayList<>(50000 + 1000); // 增加安全余量
  2. 避免陷阱

    • 场景 1:嵌套循环中的 add 操作

      java 复制代码
      // 错误示例:每次外层循环都新建 ArrayList
      for (int i=0; i<1000; i++) {
          List<Data> tempList = new ArrayList<>(); // 应复用 list
          // ... 添加操作
      }
    • 场景 2:超大对象存储

      java 复制代码
      // 建议分块存储
      List<byte[]> chunks = new ArrayList<>(100);
      for (int i=0; i<100; i++) {
          chunks.add(new byte[1_000_000]); // 每个元素占 1MB
      }
      // 总内存占用:100MB × 1.5 = 150MB
  3. 线程安全方案

    java 复制代码
    // 方案 1:同步包装
    List<String> safeList = Collections.synchronizedList(new ArrayList<>());
    
    // 方案 2:写时复制
    CopyOnWriteArrayList<String> cowList = new CopyOnWriteArrayList<>();

八、高频面试题解析

问题 1:ArrayList 和 LinkedList 如何选择?

  • ArrayList 优势

    • 随机访问 O(1)
    • 内存连续,CPU 缓存友好
  • LinkedList 适用场景

    • 频繁在任意位置插入/删除
    • 不需要随机访问

问题 2:如何实现安全缩容?

java 复制代码
// 手动缩容到实际大小
list.trimToSize(); // 将容量调整为当前 size
// 注意:频繁调用会导致内存抖动

问题 3:为什么 elementData 用 transient 修饰?

  • 序列化优化 :ArrayList 自定义了 writeObject/readObject 方法,只序列化实际存储的元素(跳过空余位置),减少传输数据量。

九、总结

理解 ArrayList 扩容机制的意义:

  1. 性能优化:通过预设容量避免多次扩容
  2. 内存管理:预估超大集合的内存占用
  3. 设计启示:学习空间换时间的经典实现
相关推荐
Chen-Edward1 分钟前
有了Spring为什么还有要Spring Boot?
java·spring boot·spring
magic3341656320 分钟前
Springboot整合MinIO文件服务(windows版本)
windows·spring boot·后端·minio·文件对象存储
开心-开心急了30 分钟前
Flask入门教程——李辉 第一、二章关键知识梳理(更新一次)
后端·python·flask
掘金码甲哥41 分钟前
调试grpc的哼哈二将,你值得拥有
后端
陈小桔1 小时前
idea中重新加载所有maven项目失败,但maven compile成功
java·maven
小学鸡!1 小时前
Spring Boot实现日志链路追踪
java·spring boot·后端
xiaogg36781 小时前
阿里云k8s1.33部署yaml和dockerfile配置文件
java·linux·kubernetes
逆光的July1 小时前
Hikari连接池
java
微风粼粼2 小时前
eclipse 导入javaweb项目,以及配置教程(傻瓜式教学)
java·ide·eclipse
番茄Salad2 小时前
Spring Boot临时解决循环依赖注入问题
java·spring boot·spring cloud