ArrayList 实现原理深度剖析

188 阅读5分钟

ArrayList 实现原理深度剖析

——动态数组的“三十六计”,从扩容心机到内存暗战


一、底层架构:数组的“变形记”

ArrayList 的核心是一个会“自我膨胀”的数组,其设计哲学可以概括为:用空间换时间,以冗余换自由

1. 核心成员变量

// 真正存储数据的数组(用 transient 修饰,序列化时有特殊处理)
transient Object[] elementData; 

// 当前元素数量(注意:不是数组长度!)
private int size;  

关键差异

  • 容量(capacity)elementData.length(数组的实际长度,像杯子的容积)。
  • 大小(size):当前存储的元素数量(杯中实际水量)。

初始容量默认10,但构造空列表时(JDK8+),数组延迟到首次添加元素时才创建(内存优化小技巧)。

2. 构造方法里的“小心机”

  • 无参构造

    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; // 指向空数组
    }
    

    首次添加元素时,才扩容到默认容量10(避免内存浪费)。

  • 指定容量构造

    public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity]; // 直接创建指定容量数组
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA; // 空数组
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity);
        }
    }
    

二、动态扩容:ArrayList 的“生存智慧”

扩容是 ArrayList 的看家本领,其过程堪比“乾坤大挪移”。

1. 扩容触发条件

// add() 方法中的扩容检查
public boolean add(E e) {
    ensureCapacityInternal(size + 1); // 确保容量足够
    elementData[size++] = e;
    return true;
}
  • 核心逻辑:当 size + 1 > elementData.length 时触发扩容(新元素无处安放)。

2. 扩容公式:1.5倍增长的数学魔法

// 计算新容量
int newCapacity = oldCapacity + (oldCapacity >> 1); // 右移1位相当于除以2
  • 示例
    • 旧容量10 → 新容量15(10 + 5)
    • 旧容量15 → 新容量22(15 + 7)
      设计意图:平衡扩容次数与空间浪费(2倍太激进,1.2倍又太频繁)。

3. 扩容全流程

  1. 计算新容量:取 旧容量 × 1.5所需最小容量 的较大值。
  2. 创建新数组Arrays.copyOf()System.arraycopy() 拷贝数据。
  3. 旧数组退役:旧数组被 GC 回收(像极了一代代被淘汰的手机)。

扩容代码片段

private void grow(int minCapacity) {
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1); // 1.5倍
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity; // 保底值
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    elementData = Arrays.copyOf(elementData, newCapacity);
}

4. 扩容性能开销

  • 时间复杂度:O(n)(需要拷贝所有元素)。
  • 空间开销:旧数组内存被短暂同时占用(内存敏感场景需警惕)。

避坑技巧

// 预先设置足够容量,避免多次扩容
List<Integer> list = new ArrayList<>(1000000); // 一次性吃饱,拒绝反复横跳

三、元素操作:数组的“刀光剑影”

1. 添加元素

  • 尾部追加:O(1)(直接放到数组末尾)。
  • 中间插入:O(n)(需要移动后续元素,像早高峰插队引发众怒)。
    public void add(int index, E element) {
        rangeCheckForAdd(index);
        ensureCapacityInternal(size + 1);
        // 关键代码:将 index 后的元素右移一位
        System.arraycopy(elementData, index, elementData, index + 1, size - index);
        elementData[index] = element;
        size++;
    }
    

2. 删除元素

  • 尾部删除:O(1)(直接 size--)。
  • 中间删除:O(n)(需要移动后续元素补位)。
    public E remove(int index) {
        rangeCheck(index);
        E oldValue = elementData(index);
        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index, numMoved);
        elementData[--size] = null; // 置空帮助 GC
        return oldValue;
    }
    

3. 随机访问

// 直接通过索引访问数组,时间复杂度 O(1)
public E get(int index) {
    rangeCheck(index);
    return elementData(index);
}

性能优势:比 LinkedList 快 N 倍(链表需要从头遍历)。


四、线程安全:ArrayList 的“阿喀琉斯之踵”

1. 多线程操作风险

  • 数据覆盖:两个线程同时执行 add(),导致元素丢失。
  • 数组越界:扩容过程中 size 未及时更新,引发 ArrayIndexOutOfBoundsException

2. 故障复现代码

List<String> list = new ArrayList<>();
// 两个线程同时执行 1000 次 add()
Runnable task = () -> {
    for (int i = 0; i < 1000; i++) {
        list.add("test");
    }
};
new Thread(task).start();
new Thread(task).start();
// 最终 size 可能小于 2000(部分数据被覆盖)

3. 解决方案

  • 加锁:使用 Collections.synchronizedList(new ArrayList<>())
  • 写时复制:使用 CopyOnWriteArrayList(适合读多写少场景)。

五、序列化策略:空间优化的“障眼法”

1. 为什么 elementData 用 transient 修饰?

  • 避免序列化空位:默认序列化会写入整个数组(包括未使用的空槽),浪费空间。

2. 自定义序列化

private void writeObject(java.io.ObjectOutputStream s) throws IOException {
    // 只序列化实际存储的元素
    s.defaultWriteObject();
    s.writeInt(size);
    for (int i=0; i<size; i++) {
        s.writeObject(elementData[i]);
    }
}

private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException {
    // 反序列化时重建数组
    elementData = EMPTY_ELEMENTDATA;
    s.defaultReadObject();
    s.readInt(); // ignored
    // 根据 size 重建数组
}

效果:序列化后的数据量减少(像压缩行李箱,只带必需品)。


六、迭代器设计:fail-fast 机制的“无间道”

1. modCount 的使命

  • 修改计数器:记录列表结构修改次数(add/remove 等操作会递增)。

2. 迭代过程中的安全检查

// ArrayList.Itr 迭代器源码
final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

经典错误

List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
for (String s : list) {
    if (s.equals("B")) list.remove(s); // 抛出 ConcurrentModificationException
}

正确做法

Iterator<String> it = list.iterator();
while (it.hasNext()) {
    if (it.next().equals("B")) it.remove(); // 安全删除
}

七、性能优化:ArrayList 的“内功心法”

1. 预分配容量

  • 直接设置初始容量new ArrayList<>(N),避免多次扩容。
  • 提前扩容ensureCapacity(int minCapacity) 预先扩容。

2. 批量操作优先

  • 用 addAll() 替代循环 add():减少扩容次数。
  • 用 clear() 替代循环 remove():直接置空数组(O(1) vs O(n))。

3. 及时释放空间

// 释放未使用的容量(像减肥后扔掉大号衣服)
list.trimToSize();

八、终极思考:为什么 ArrayList 不是完美无缺的?

  1. 增删效率低:数据搬运成本高(适合读多写少场景)。
  2. 内存碎片风险:频繁扩容可能引发 Full GC。
  3. 线程安全代价:同步锁或写时复制会降低性能。

总结
ArrayList 是动态数组的典范之作,其精妙的设计在空间与时间、性能与安全之间找到了优雅的平衡。理解其原理不仅是面试通关的钥匙,更是写出高性能代码的基石。下次当你写下 new ArrayList<>() 时,请记住——这行简单的代码背后,藏着一场数组与算法的华丽共舞!

彩蛋
若面试官问“为什么 ArrayList 的迭代器比 for 循环快?”,请回答:“因为迭代器直接访问数组,而 for 循环每次调用 get() 要检查索引范围!”(真实原因:get() 方法中有 rangeCheck(index) 检查)。