1. 概述
本文将简要分析 Java 数组与标准 ArrayList
在内存分配上的异同,并重点讲解如何向数组和 ArrayList
中追加或插入元素。
对于有经验的开发者来说,这属于基础但高频踩坑的场景 —— 特别是在性能敏感或频繁操作集合的场景下,选择不当会带来不必要的对象创建和内存拷贝开销。
2. Java 数组与 ArrayList
Java 数组是语言层面提供的基础数据结构,而 ArrayList
是 Java 集合框架中 List
接口的一个基于数组的实现类。
两者虽然底层都依赖数组存储数据,但在使用方式、灵活性和性能表现上存在显著差异。
2.1 访问与修改元素
数组使用方括号语法直接访问或修改元素,简单粗暴:
System.out.println(anArray[1]);
anArray[1] = 4;
而 ArrayList
提供了封装好的方法:
int n = anArrayList.get(1);
anArrayList.set(1, 4);
✅ 优势:ArrayList
方法调用更安全(边界检查),代码可读性更强
❌ 劣势:多了方法调用开销,对性能极致要求的场景可能成为瓶颈
2.2 固定大小 vs 动态扩容
这是两者最核心的区别:
- ✅ 数组:固定大小,创建时必须指定长度,之后无法扩容
- ✅ ArrayList:动态扩容,内部数组满时自动创建更大的数组并复制元素
当 ArrayList
中元素数量超过其内部数组容量时,会触发扩容机制。具体策略由其实现决定(通常是 1.5 倍增长),但本质是:
- 创建一个新的更大数组
- 将旧数组所有元素复制过去
- 释放旧数组引用
⚠️ 虽然单次 add
操作平均时间复杂度为 **O(1)**(摊还分析),但个别扩容操作是 O(n) 的代价。频繁添加元素时建议预设初始容量以避免反复扩容。
2.3 元素类型支持
- 数组:支持原始类型(如
int[]
)和引用类型(如String[]
) - ArrayList:只能存放引用类型,不支持原始类型
这意味着当你写:
ArrayList<Integer> list = new ArrayList<>();
list.add(42); // 自动装箱:int → Integer
Java 编译器会自动进行自动装箱(autoboxing),将 int
转为 Integer
对象。这在高频操作中可能带来额外的 GC 压力。
⚠️ 小心踩坑:频繁使用 ArrayList<Integer>
处理大量数值时,考虑用 TIntArrayList
(来自 Trove 等第三方库)替代,避免装箱开销。
3. 追加元素
数组:手动扩容 + 复制
由于数组大小固定,追加元素需手动完成三步:
- 创建新数组(原长度 +1)
- 复制原数组内容
- 在末尾添加新元素
纯 Java 实现如下:
public Integer[] addElementUsingPureJava(Integer[] srcArray, int elementToAdd) {
Integer[] destArray = new Integer[srcArray.length + 1];
for (int i = 0; i < srcArray.length; i++) {
destArray[i] = srcArray[i];
}
destArray[destArray.length - 1] = elementToAdd;
return destArray;
}
更简洁的方式是使用 Arrays.copyOf()
:
int[] destArray = Arrays.copyOf(srcArray, srcArray.length + 1);
destArray[destArray.length - 1] = elementToAdd;
✅ 推荐:Arrays.copyOf()
底层调用 System.arraycopy()
,效率更高
ArrayList:一行搞定
anArrayList.add(newElement);
✅ 简单到没朋友,内部自动处理扩容逻辑
⚠️ 注意:如果知道最终元素数量,建议构造时指定初始容量,避免多次扩容
4. 在指定索引插入元素
数组:手动搬移 + 插入
在数组中插入元素需:
- 创建更大数组
- 遍历并判断位置,插入点前正常复制,插入点后整体右移一位
示例代码:
public static int[] insertAnElementAtAGivenIndex(final int[] srcArray, int index, int newElement) {
int[] destArray = new int[srcArray.length + 1];
int j = 0;
for (int i = 0; i < destArray.length; i++) {
if (i == index) {
destArray[i] = newElement;
} else {
destArray[i] = srcArray[j];
j++;
}
}
return destArray;
}
测试验证:
int[] expectedArray = { 1, 2, 42, 3, 4 };
int[] anArray = { 1, 2, 3, 4 };
int[] outputArray = ArrayOperations.insertAnElementAtAGivenIndex(anArray, 2, 42);
assertThat(outputArray).containsExactly(expectedArray);
使用 Apache Commons Lang 简化操作
ArrayUtils.insert()
方法让这事变得轻松:
int[] destArray = ArrayUtils.insert(2, srcArray, 77);
参数说明:
- 第一个参数:插入位置(index)
- 第二个参数:原数组
- 第三个及以后:要插入的值(支持可变参数)
例如插入多个元素:
int[] destArray = ArrayUtils.insert(2, srcArray, 77, 88, 99);
结果:原数组从 index=2 开始的元素全部右移 3 位。
ArrayList:原生支持
anArrayList.add(index, newElement);
内部自动完成元素右移,开发者无感知。
✅ 推荐场景:需要频繁在中间插入元素时,优先选 ArrayList
而非数组
5. 在开头插入元素(Prepend)
即在索引 0
处插入元素。
数组实现方式一:复用插入逻辑
直接调用前面写的插入方法:
int[] result = ArrayOperations.insertAnElementAtAGivenIndex(anArray, 0, 42);
方式二:使用 System.arraycopy
更高效,避免手动循环:
public static int[] prependAnElementToArray(int[] srcArray, int element) {
int[] newArray = new int[srcArray.length + 1];
newArray[0] = element;
System.arraycopy(srcArray, 0, newArray, 1, srcArray.length);
return newArray;
}
✅ System.arraycopy()
是 JVM 内部优化过的本地方法,性能优于手动 for 循环
方式三:Apache Commons Lang 快捷方法
int[] result = ArrayUtils.addFirst(anArray, 42);
底层其实还是调用 ArrayUtils.insert(0, array, element)
,但语义更清晰。
ArrayList 对应操作
anArrayList.add(0, newElement);
⚠️ 注意:在 ArrayList
头部插入效率较低(O(n)),因为所有后续元素都要右移。若频繁在头部操作,建议改用 LinkedList
6. 总结
特性 | 数组 | ArrayList |
---|---|---|
✅ 大小 | 固定 | 动态扩容 |
✅ 元素类型 | 支持原始类型 | 仅引用类型(自动装箱) |
✅ 追加元素 | 手动扩容 + 复制 | add() 一行解决 |
✅ 中间插入 | 复杂,需手动搬移 | add(index, elem) 原生支持 |
✅ 性能 | 高效,无额外开销 | 存在对象封装、扩容成本 |
✅ 使用场景 | 固定大小、高性能数值处理 | 动态集合、频繁增删 |
✅ 最佳实践建议:
- 如果集合大小已知且不变 → 用数组
- 如果需要频繁添加/删除元素 → 用
ArrayList
- 避免在
ArrayList
头部频繁插入 → 考虑LinkedList
- 大量原始类型操作 → 考虑
Trove
、FastUtil
等第三方库减少装箱开销 - 使用
Arrays.copyOf()
或System.arraycopy()
优化数组复制 - 使用 Apache Commons Lang 的
ArrayUtils
简化常见操作
示例完整源码已托管至 GitHub:https://github.com/baeldung/core-java-modules/tree/core-java-arrays-operations-basic