【Java集合】ArrayList :底层原理、数组互转与扩容计算

以下是 ArrayList 的底层原理的详细介绍,结合其动态数组实现、扩容机制、核心操作及线程安全性等方面展开:

一、底层结构与初始化

ArrayList 的底层基于 Object 数组 **elementData** 实现 ,通过动态扩容机制 支持元素的动态增删。

• 核心属性:

• elementData:存储元素的数组,初始为空数组(无参构造时)。

• size:记录当前元素数量(非数组长度)。

• 初始容量: 无参构造默认空数组 (JDK7 是 10,JDK8 优化为懒加载,在空参构造时创建一个ArrayList时0,首次 add 时扩容至 10 );

• 可通过构造器指定初始容量(如 new ArrayList<>(50)),避免频繁扩容。

• 扩容本质:通过Arrays.copyOf()复制原数组到新数组,耗时且产生内存碎片,因此批量添加建议先用ensureCapacity()预扩容;

二、动态扩容机制

  1. 扩容规则:

• 初始容量: 无参构造默认空数组 (JDK7 是 10,JDK8 优化为懒加载,在空参构造时创建一个ArrayList时0,首次 add 时扩容至 10);

• 触发时机:添加元素时,若 size + 1 > elementData.length,则调用 grow() 扩容;

• 新容量 = 旧容量 × 1.5**(通过** **oldCapacity + (oldCapacity >> 1)** 实现右移运算 0.5倍) 。

• 若一次性添加多个元素(如 addAll()),直接扩容至 Math.max(原容量 × 1.5, 实际所需容量)。

  1. 扩容实现:
    • 调用 Arrays.copyOf() 将旧数组数据复制到新数组 ,旧数组被 GC 回收。
    • 最大容量限制为 Integer.MAX_VALUE - 8(避免内存溢出)。

代码debug调试:

java 复制代码
@Test
public void t1() {
    //初始化容量:0
    ArrayList<Integer> list = new ArrayList<>();
    //第一次初始化:初始化容量为 10
    list.add(1);
    //当已经使用的数组长度大于当前数组长度(10)
    //第11次添加:调用grow方法扩容 1.5倍  变10x1.5=15
    for(int i = 1;i<10;i++){
        list.add(i+1);
    }
    list.add(100);
}

三、核心操作的时间复杂度

操作 时间复杂度 说明
尾部追加元素 O(1) 均摊 扩容时需 O(n),但均摊后为 O(1)。
随机访问 O(1) 直接通过数组索引访问(如 get(index))。
插入/删除元素 O(n) 需移动后续元素(如 add(index, e) 或 remove(index))。
遍历 O(n) 顺序访问数组元素。

关于查找的时间复杂度:

  • 下标查找:O(1) 直接返回
  • 未排序查找:O(n) 遍历
  • 排序查找:O(logn) 二分

四、数组获取元素地址原理

ai=数组首地址+下标(索引)*数组元素类型长度

关于为什么不从1开始?(多一次减法操作)

五、线程安全性与替代方案

• 非线程安全 :

• 多线程并发修改会导致数据不一致或 ConcurrentModificationException(通过 modCount 检测修改次数)。

• 解决方案:

• 使用 Collections.synchronizedList(new ArrayList<>()) 包装为同步列表。

• 高并发读场景下,采用 CopyOnWriteArrayList(写时复制,牺牲一致性)。

六、与 Vector 的对比

特性 ArrayList Vector
线程安全性 非线程安全 线程安全(方法用 synchronized修饰)。
扩容机制 扩容 1.5 倍 扩容 2 倍。
性能 更高(无锁开销) 较低(锁竞争)。
初始容量 默认 10(延迟初始化) 默认 10(直接初始化)。

七、数组与 List 互转的坑点

1. Arrays.asList() 数组转 List

  • 核心特性 :返回的是 Arrays 内部类 ArrayList(不是 java.util.ArrayList),底层直接引用原数组,共享同一块内存地址。

  • 坑点:

    • 修改原数组元素 → List 内容同步改变(受影响);
    • 不能调用 add/remove 方法,会抛 UnsupportedOperationException(内部类未实现增删)。
  • 正确用法 :new ArrayList<>(Arrays.asList(strs)) 包装成真正的 java.util.ArrayList,切断与原数组的关联。

2. List.toArray() List 转数组

  • 核心特性 :底层会拷贝一份新数组,与原 List 完全隔离。

  • 坑点:

    • 修改 List 元素 → 数组内容不会改变(不受影响);
    • 推荐写法:list.toArray(new String[list.size()]),避免无参 toArray() 返回 Object[] 强转风险。

八、源码关键方法

  1. 添加元素(add(e)):
java 复制代码
public boolean add(E e) {     
    ensureCapacityInternal(size + 1); // 检查扩容
    elementData[size++] = e; // 尾部插入
    return true;
}
  • 检查容量:判断 size + 1 是否超过当前数组长度,不足则先扩容;
  • 赋值:将新元素放到 elementData[size] 位置,size 自增 1;
  • 返回:返回 true 表示添加成功。

• ensureCapacityInternal() 确保容量足够,触发扩容逻辑。

  1. 扩容(grow()):
java 复制代码
private void grow(int minCapacity) {  
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1); // 1.5倍扩容
    elementData = Arrays.copyOf(elementData, newCapacity); // 复制数组
}

面试必会

new ArrayList(10) 扩容几次

  • 答案 :0 次,不会扩容。
  • 原理:
  • 调用有参构造时,直接创建 new Object[10] 赋值给 elementData;
  • 只有当 size + 1 > elementData.length 时才会触发 grow(),初始容量 10 足够容纳前 10 个元素,因此前 10 次添加都不会扩容。
  • 延伸:
  • 第 11 次添加时,size + 1 = 11 > 10,触发第一次扩容到 15;
  • 第 16 次添加时,触发第二次扩容到 22(15 + 15>>1 = 22)。

面试官:ArrayList 底层原理是什么?

答:

  • ArrayList 底层是动态 Object 数组

  • 初始化容量:JDK7 是 10,JDK8 优化为懒加载,ArrayList初始容量为0,首次 add 时扩容至 10

  • 有参构造 new ArrayList(10) 直接创建长度 10 的数组,不会扩容。

  • 扩容:扩容到原来容量的1.5倍,每次扩容需要数组拷贝

  • 添加元素:

    • 检查容量:判断 size + 1 是否超过当前数组长度,不足则先扩容;
    • 赋值:将新元素放到 elementData[size] 位置,size 自增 1;
    • 返回:返回 true 表示添加成功。
  • 非线程安全。

面试官:Arrays.asList 转 List 后改数组,List 会变吗?

答:会变,因为 Arrays.asList 返回的内部类 ArrayList 直接引用原数组,共享内存;而 List.toArray 会拷贝新数组,改 List 不影响数组。

面试官:new ArrayList (10) 扩容几次?

答:0 次,因为构造时直接创建了长度为 10 的数组,前 10 次添加都不会触发扩容。

相关推荐
宸津-代码粉碎机2 小时前
OpenAI 连夜迎战 Grok Bot 和 Muse:AI 智能体从 “会聊天” 到 “能办事”,现在入场还来得及吗
java·大数据·人工智能·分布式·python
Y3815326622 小时前
SERP API 计费口径:credits_charged、执行失败扣费与退款边界
python·搜索引擎
weixin_511255212 小时前
MySQL升级记录
数据库·mysql
chenbingjie_c2 小时前
C++ 进阶核心知识点总结:模板、内存分区、new/delete、内存池
开发语言·c++
噢,我明白了3 小时前
java中唯一键和幂等键的应用
java·后端
程序猿乐锅3 小时前
【黑马点评 | 第八篇】Redisson分布式锁
java·数据库·spring boot·redis·分布式·spring·缓存
专注仿真3 小时前
ACM算法
算法·深度优先
GreenTea3 小时前
我把 Agent 的 while 循环拆掉了:一种你可能没想到的 Agent 架构
前端·后端·架构
Leo.yuan3 小时前
2026年本地化Data Agent优质厂商盘点:哪些产品更适合企业生产环境
大数据·数据库·人工智能
考虑考虑3 小时前
synchronized字符串常量
java·后端·java ee