欢迎光临
我们一直在努力

ArrayList动态扩容机制

在 Java 集合框架中,ArrayList 是我们日常开发中最常用的动态数组实现。它底层基于数组实现,却能在元素数量超过容量时自动扩容,让我们无需手动管理数组大小。今天我们就来拆解 ArrayList 的动态扩容机制,帮你彻底搞懂它的工作原理与性能优化点。


一、核心概念:容量 vs 大小

在聊扩容之前,我们先分清两个容易混淆的概念:

  • 容量(Capacity):底层数组 elementData 的长度,代表 ArrayList 最多能容纳多少元素。
  • 大小(Size):当前实际存储的元素个数,通过 size() 方法获取。

举个例子:一个刚创建的 ArrayList,容量可能是 10,但如果还没添加任何元素,它的大小就是 0。


二、扩容触发时机

扩容并不是随时发生的,只有在添加元素时才会触发:

当我们调用 add(E e) 或 addAll() 等方法时,ArrayList 会先检查:

如果 size + 1 > 当前容量,就说明数组放不下新元素了,必须触发扩容。

简单来说:装不下了,才扩容。


三、JDK 8 扩容完整流程

我们以 JDK 8 为例,一步步拆解扩容的核心逻辑。

1. 入口:add() 方法

add() 是扩容的起点,核心代码如下:

public boolean add(E e) {
// 第一步:确保容量足够
ensureCapacityInternal(size + 1);
// 第二步:赋值并自增 size
elementData[size++] = e;
return true;
}

可以看到,添加元素分为两步:

  • 先调用 ensureCapacityInternal 检查并扩容
  • 再将元素放入数组并更新 size
  • 2. 容量检查:ensureCapacityInternal

    这个方法会处理初始容量的特殊情况,并调用真正的扩容判断逻辑:

    private void ensureCapacityInternal(int minCapacity) {
    // 如果是刚创建的空数组,首次扩容直接用默认容量 10
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
    minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    ensureExplicitCapacity(minCapacity);
    }

    private void ensureExplicitCapacity(int minCapacity) {
    modCount++; // 记录集合修改次数(用于迭代器快速失败)
    // 如果需要的最小容量 > 当前数组长度,才执行扩容
    if (minCapacity – elementData.length > 0)
    grow(minCapacity);
    }

    3. 核心扩容:grow() 方法

    grow() 是扩容的核心,负责计算新容量并创建新数组:

    private void grow(int minCapacity) {
    // 1. 获取旧容量
    int oldCapacity = elementData.length;
    // 2. 新容量 = 旧容量 + 旧容量 / 2(即扩容 1.5 倍)
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    // 3. 特殊情况:如果 1.5 倍扩容后仍不够,直接用需要的最小容量
    if (newCapacity – minCapacity < 0)
    newCapacity = minCapacity;
    // 4. 超大容量处理:防止超过 Integer.MAX_VALUE
    if (newCapacity – MAX_ARRAY_SIZE > 0)
    newCapacity = hugeCapacity(minCapacity);
    // 5. 复制原数组到新数组(性能开销最大的一步)
    elementData = Arrays.copyOf(elementData, newCapacity);
    }

    4. 关键细节总结

    • 默认初始容量:new ArrayList<>() 创建的空数组,首次添加元素时容量会直接扩容到 10。
    • 默认扩容比例:每次扩容为原容量的 1.5 倍(oldCapacity + oldCapacity/2)。
    • 兜底策略:如果 1.5 倍扩容后仍无法满足需求,会直接使用需要的最小容量。
    • 容量上限:数组最大长度为 Integer.MAX_VALUE – 8,极端情况会使用 Integer.MAX_VALUE。

    四、代码示例:直观感受扩容变化

    我们通过反射来查看 ArrayList 底层数组的容量变化:

    import java.util.ArrayList;
    import java.lang.reflect.Field;

    public class ArrayListExpandDemo {
    public static void main(String[] args) throws Exception {
    ArrayList<String> list = new ArrayList<>();
    Field field = ArrayList.class.getDeclaredField("elementData");
    field.setAccessible(true);

    // 初始状态:空数组,容量 0
    System.out.println("初始容量:" + ((Object[]) field.get(list)).length); // 0

    // 添加第 1 个元素:扩容到 10
    list.add("a");
    System.out.println("添加1个元素后容量:" + ((Object[]) field.get(list)).length); // 10

    // 添加到第 10 个元素:容量保持 10
    for (int i = 1; i < 10; i++) list.add("a" + i);
    System.out.println("添加10个元素后容量:" + ((Object[]) field.get(list)).length); // 10

    // 添加第 11 个元素:扩容到 15(10 + 10/2)
    list.add("b");
    System.out.println("添加11个元素后容量:" + ((Object[]) field.get(list)).length); // 15
    }
    }

    运行结果:

    初始容量:0
    添加1个元素后容量:10
    添加10个元素后容量:10
    添加11个元素后容量:15


    五、性能影响与优化建议

    1. 扩容的性能开销

    扩容的核心是 Arrays.copyOf(),它需要遍历原数组并将所有元素复制到新数组中。元素越多,扩容耗时越长,频繁扩容会严重影响性能。

    2. 优化方案

    • 提前指定初始容量:如果已知元素数量(比如要存 1000 个元素),创建时直接 new ArrayList<>(1000),避免多次扩容。
    • 手动触发扩容:不确定数量时,可调用 ensureCapacity(int minCapacity) 手动扩容,减少后续扩容次数。

    // 示例:提前扩容到 1000
    ArrayList<String> list = new ArrayList<>();
    list.ensureCapacity(1000);


    六、面试高频考点

  • ArrayList 默认初始容量是多少?答:JDK 7+ 中,new ArrayList<>() 初始是空数组,首次添加元素时扩容到 10。
  • ArrayList 每次扩容多少倍?答:默认扩容为原容量的 1.5 倍。
  • 为什么 ArrayList 不设计成线程安全的?答:为了保证单线程下的高性能,线程安全可以用 CopyOnWriteArrayList 或 Collections.synchronizedList() 替代。
  • 如何优化 ArrayList 性能?答:提前指定初始容量,避免频繁扩容;尽量在尾部添加 / 删除元素,中间操作会触发数组复制。
  • 赞(0)
    未经允许不得转载:171主机测评 » ArrayList动态扩容机制
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址