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. 设计启示:学习空间换时间的经典实现
相关推荐
&岁月不待人&几秒前
Android 常用设计模式和实例
java·开发语言·设计模式
qq_13948428829 分钟前
springboot239-springboot在线医疗问答平台(源码+论文+PPT+部署讲解等)
java·数据库·spring boot·后端·spring·maven·intellij-idea
蔚一10 分钟前
微服务SpringCloud Alibaba组件nacos教程【详解naocs基础使用、服务中心配置、集群配置,附有案例+示例代码】
java·后端·spring cloud·微服务·架构·intellij-idea·springboot
神仙别闹13 分钟前
基于Springmvc+MyBatis+Spring+Bootstrap+EasyUI+Mysql的个人博客系统
java·mysql·ssm
计算机小白一个16 分钟前
蓝桥杯 Java B 组之函数定义与递归入门
java·算法·职场和发展·蓝桥杯
Hello.Reader42 分钟前
将错误消息输出到标准错误流:Rust中的最佳实践
开发语言·后端·rust
Asthenia04121 小时前
深入理解 Java 线程池:参数、拒绝策略与常见问题
后端
用键盘当武器的秋刀鱼1 小时前
Spring boot(maven) - Mybatis 超级入门版
java·spring boot·mybatis
管大虾1 小时前
设计模式-适配器模式
java·设计模式·适配器模式