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 获取。