1. 概述

Java 提供了两种方式来处理一组对象:固定大小的数组(Array)和动态集合类(如 ArrayList)。虽然两者都能存储多个元素,但在内存管理、性能表现等方面存在显著差异。

本文将重点对比 Java 数组的大小(size)ArrayList 的容量(capacity),并分析在什么场景下手动设置 ArrayList 初始容量是值得推荐的做法。

✅ 你会学到:

  • 数组与 ArrayList 在容量上的区别
  • Java 8 中 ArrayList 容量机制的变化
  • 如何通过反射获取 ArrayList 的真实容量
  • 初始化容量对性能和内存的影响

2. 示例说明

我们先从一个简单的例子入手,看看数组和 ArrayList 在创建时的行为有何不同。

2.1. 数组的大小

Java 中创建数组时必须明确指定其大小:

Integer[] array = new Integer[100]; 
System.out.println("Size of an array:" + array.length);

输出结果为:

Size of an array:100

也就是说,一旦创建完成,数组的大小就固定不变了。

2.2. ArrayList 的容量

ArrayList 是动态扩容的集合,默认构造器创建的对象初始容量为 0(⚠️注意:不是 10!),这是 Java 8 引入的优化手段。

来看一下官方源码中的默认容量定义:

private static final int DEFAULT_CAPACITY = 10;

但是这个值并不是立即分配的。只有当第一次添加元素时才会真正分配长度为 10 的底层数组。

我们可以通过反射来查看当前 ArrayList 的实际容量:

public static int getDefaultCapacity(ArrayList<?> arrayList) throws Exception {

    if (arrayList == null) {
        return 0;
    }

    Field field = ArrayList.class.getDeclaredField("elementData");
    field.setAccessible(true);

    return ((Object[]) field.get(arrayList)).length;
}

验证一下空 ArrayList 的容量是否为 0:

@Test
void givenEmptyArrayList_whenGetDefaultCapacity_thenReturnZero() throws Exception {

    ArrayList<Integer> myList = new ArrayList<>();
    int defaultCapacity = DefaultArrayListCapacity.getDefaultCapacity(myList);

    assertEquals(0, defaultCapacity);
}

接着添加一个元素再检查容量:

@Test
void givenEmptyArrayList_whenAddItemAndGetDefaultCapacity_thenReturn10() throws Exception {

    ArrayList<String> myList = new ArrayList<>();
    myList.add("ITEM 1");

    int defaultCapacity = DefaultArrayListCapacity.getDefaultCapacity(myList);

    assertEquals(10, defaultCapacity);
}

✅ 这个懒加载机制的目的就是为了节省内存资源。

如果我们显式指定初始容量呢?

List<Integer> list = new ArrayList<>(100);
assertEquals(0, list.size());

此时虽然底层数组已经准备好了空间,但逻辑大小仍为 0。

加入一个元素后:

list.add(10);
assertEquals(1, list.size());

3. 数组大小 vs ArrayList 容量的区别

3.1. 大小是否可变?

❌ 数组:大小固定,初始化后无法更改。

✅ ArrayList:逻辑大小可变,支持动态增删元素;物理容量会自动扩展。

3.2. 内存分配时机?

❌ 数组:创建时一次性分配全部内存。

✅ ArrayList:初始容量为 0,首次添加元素时才分配底层数组;后续容量不足时再重新分配更大数组并复制旧数据。

⚠️ 特别注意:ArrayList 内部使用的是 Object[] 类型的数组,即使你声明的是泛型类型。


4. 什么时候应该手动设置 ArrayList 初始容量?

虽然大多数情况下可以不指定初始容量,但某些场景下手动设置是有益的。

4.1. 构建大型 ArrayList

如果你知道最终需要插入大量元素,提前设置初始容量可以避免频繁的扩容操作,从而提升性能。

比如:

List<String> list = new ArrayList<>(10000);

否则每次容量不够时都会触发扩容(通常是 1.5 倍增长),带来额外的时间开销和内存浪费。

4.2. 构建大量小型 ArrayList

如果应用中存在成千上万个小型 ArrayList 实例(例如每个只放 2~3 个元素),而默认容量又是 10,则会造成高达 70% 的内存浪费。

这种情况下建议手动设置容量以节省内存。


5. 避免不必要的内存浪费

虽然 ArrayList 提供了灵活的操作接口,但也带来了额外的内存开销。

✅ 适合使用 ArrayList 的场景:

  • 支持随机访问(通过索引)
  • 元素数量变化较大
  • 不追求极致性能

❌ 不适合使用的场景:

  • 存储大量基本类型数据(考虑使用数组或 TIntArrayList 等原生集合)
  • 不需要按索引访问(考虑使用 LinkedList

6. 总结

特性 Array ArrayList
大小是否可变 ❌ 固定 ✅ 可变
初始容量 必须指定 默认 0(首次 add 后为 10)
内存分配时机 创建时 首次 add 时
是否支持扩容 ❌ 否 ✅ 是
是否支持泛型 ✅ 是 ✅ 是
是否支持 null 元素 ✅ 是 ✅ 是

📌 结论

  • 如果你知道数据规模,初始化 ArrayList 时指定容量是一个好习惯。
  • 对于小规模数据或者频繁变动的结构,ArrayList 更合适。
  • 如果追求极致性能和内存效率,数组仍是不可替代的选择。

📌 示例代码可在 GitHub 获取。


原始标题:The Capacity of an ArrayList vs the Size of an Array in Java | Baeldung