以下是 ArrayList 的底层原理的详细介绍,结合其动态数组实现、扩容机制、核心操作及线程安全性等方面展开:
一、底层结构与初始化
ArrayList 的底层基于 Object 数组 elementData
实现,通过动态扩容机制支持元素的动态增删。
• 核心属性:
• elementData
:存储元素的数组,初始为空数组(无参构造时)。
• size
:记录当前元素数量(非数组长度)。
• 初始容量:
• 默认初始容量为 10(首次添加元素时触发,在空参构造时创建一个ArrayList时默认是容量为0的)。
• 可通过构造器指定初始容量(如 new ArrayList<>(50)
),避免频繁扩容。
二、动态扩容机制
当添加元素导致 size + 1 > elementData.length
时触发扩容:
- 扩容规则:
• 新容量 = 旧容量 × 1.5(通过oldCapacity + (oldCapacity >> 1)
实现右移运算)。
• 若一次性添加多个元素(如addAll()
),直接扩容至Math.max(原容量 × 1.5, 实际所需容量)
。 - 扩容实现:
• 调用Arrays.copyOf()
将旧数组数据复制到新数组,旧数组被 GC 回收。
• 最大容量限制为Integer.MAX_VALUE - 8
(避免内存溢出)。
三、核心操作的时间复杂度
操作 | 时间复杂度 | 说明 |
---|---|---|
尾部追加元素 | O(1) 均摊 | 扩容时需 O(n),但均摊后为 O(1)。 |
随机访问 | O(1) | 直接通过数组索引访问(如 get(index) )。 |
插入/删除元素 | O(n) | 需移动后续元素(如 add(index, e) 或 remove(index) )。 |
遍历 | O(n) | 顺序访问数组元素。 |
四、线程安全性与替代方案
• 非线程安全:
• 多线程并发修改会导致数据不一致或 ConcurrentModificationException
(通过 modCount
检测修改次数)。
• 解决方案:
• 使用 Collections.synchronizedList(new ArrayList<>())
包装为同步列表。
• 高并发读场景下,采用 CopyOnWriteArrayList
(写时复制,牺牲一致性)。
五、与 Vector 的对比
特性 | ArrayList | Vector |
---|---|---|
线程安全性 | 非线程安全 | 线程安全(方法用 synchronized 修饰)。 |
扩容机制 | 扩容 1.5 倍 | 扩容 2 倍。 |
性能 | 更高(无锁开销) | 较低(锁竞争)。 |
初始容量 | 默认 10(延迟初始化) | 默认 10(直接初始化)。 |
六、性能优化建议
- 预分配容量:
• 若已知数据量,通过构造器指定初始容量(如new ArrayList<>(1000)
),减少扩容次数。 - 尾部操作优先:
• 避免频繁在中间插入/删除元素,尽量使用add(e)
和remove(size-1)
。 - 慎用快速失败迭代器:
• 遍历时若需修改集合,改用Iterator.remove()
或复制新集合操作。
七、源码关键方法解析
-
添加元素(
add(e)
):public boolean add(E e) { ensureCapacityInternal(size + 1); // 检查扩容 elementData[size++] = e; // 尾部插入 return true; }
•
ensureCapacityInternal()
确保容量足够,触发扩容逻辑。 -
扩容(
grow()
):private void grow(int minCapacity) { int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1); // 1.5倍扩容 elementData = Arrays.copyOf(elementData, newCapacity); // 复制数组 }
总结
ArrayList 通过动态数组实现高效的随机访问,但插入/删除中间元素性能较低,适合读多写少的场景。在多线程环境下需使用同步包装类或并发集合替代。合理预分配容量和优化操作位置可显著提升性能。