以下是 ArrayList 的底层原理的详细介绍,结合其动态数组实现、扩容机制、核心操作及线程安全性等方面展开:
一、底层结构与初始化
ArrayList 的底层基于 Object 数组 **elementData** 实现 ,通过动态扩容机制 支持元素的动态增删。
• 核心属性:
• elementData:存储元素的数组,初始为空数组(无参构造时)。
• size:记录当前元素数量(非数组长度)。
• 初始容量: 无参构造默认空数组 (JDK7 是 10,JDK8 优化为懒加载,在空参构造时创建一个ArrayList时0,首次 add 时扩容至 10 );
• 可通过构造器指定初始容量(如 new ArrayList<>(50)),避免频繁扩容。
• 扩容本质:通过Arrays.copyOf()复制原数组到新数组,耗时且产生内存碎片,因此批量添加建议先用ensureCapacity()预扩容;
二、动态扩容机制


- 扩容规则:
• 初始容量: 无参构造默认空数组 (JDK7 是 10,JDK8 优化为懒加载,在空参构造时创建一个ArrayList时0,首次 add 时扩容至 10);
• 触发时机:添加元素时,若 size + 1 > elementData.length,则调用 grow() 扩容;
• 新容量 = 旧容量 × 1.5**(通过** **oldCapacity + (oldCapacity >> 1)** 实现右移运算 0.5倍) 。
• 若一次性添加多个元素(如 addAll()),直接扩容至 Math.max(原容量 × 1.5, 实际所需容量)。
- 扩容实现:
• 调用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) 二分
四、数组获取元素地址原理

a[i]=数组首地址+下标(索引)*数组元素类型长度
关于为什么不从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[]强转风险。
八、源码关键方法
- 添加元素(
add(e)):
java
public boolean add(E e) {
ensureCapacityInternal(size + 1); // 检查扩容
elementData[size++] = e; // 尾部插入
return true;
}
- 检查容量:判断
size + 1是否超过当前数组长度,不足则先扩容; - 赋值:将新元素放到
elementData[size]位置,size自增 1; - 返回:返回
true表示添加成功。
• ensureCapacityInternal() 确保容量足够,触发扩容逻辑。
- 扩容(
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 次添加都不会触发扩容。