【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 次添加都不会触发扩容。

相关推荐
行者全栈架构师几秒前
【鸿蒙心迹】让应用“悬浮“起来——闪控球(floatingBall)从接入到实战全链路(HarmonyOS 7.x)
前端·算法·架构
北风toto几秒前
Linux 服务器如何判断 CPU 是否支持 x86-64-v2 指令集
java·linux·服务器
1024奇点1 分钟前
【仓颉语言入门 · 第二篇 第 8 课】
开发语言·数据库·ide·mysql·开源
MetWeave3 分钟前
从零跑一个气象数据管道:抓取、清洗、对账、节奏
后端·python
萧瑟余晖4 分钟前
Spring Security综合实战与安全加固详解
java·安全·spring
云的雨 雨的云5 分钟前
手写算法动画上位机:从排序到线性代数可视化
线性代数·算法
用户38514085394715 分钟前
Python 的 O_NOFOLLOW 为什么挡不住中间目录的符号链接?
python
gopher951116 分钟前
RUST:序列化与反序列化 serde + derive 详解
开发语言·后端·rust
铁皮饭盒17 分钟前
CPU就行, 老照片变清晰, 本地部署582mb腾讯的模型, 本地部署, 过节了给长辈们变个小魔术
前端·javascript·后端
imDwAaY18 分钟前
操作系统如何管理程序运行?从线程、进程到内存隔离与用户态切换:CS162 Note2
android·java·redis