1. 简介
java.util.Arrays
是自 Java 1.2 起就存在的工具类,属于 Java 核心类库中的常青树。它提供了创建、比较、排序、查找、流式处理和转换数组的一系列静态方法。
对于有经验的开发者来说,Arrays
类几乎是日常编码中绕不开的工具。本文将带你系统梳理其核心能力,避免踩坑,提升编码效率。
2. 数组创建
2.1 copyOf
与 copyOfRange
这两个方法用于从已有数组复制出新数组,区别在于:
copyOfRange(T[] original, int from, int to)
:复制指定范围(左闭右开)copyOf(T[] original, int newLength)
:复制整个数组并指定新长度
✅ 示例:
String[] intro = new String[] { "once", "upon", "a", "time" };
String[] abridgement = Arrays.copyOfRange(intro, 0, 3);
assertArrayEquals(new String[] { "once", "upon", "a" }, abridgement);
assertFalse(Arrays.equals(intro, abridgement));
String[] revised = Arrays.copyOf(intro, 3);
String[] expanded = Arrays.copyOf(intro, 5);
assertArrayEquals(Arrays.copyOfRange(intro, 0, 3), revised);
assertNull(expanded[4]); // ⚠️ 扩容时自动填充 null
⚠️ 注意:copyOf
在目标长度大于原数组时,会用 null
(引用类型)或默认值(如 0
、false
)填充。
2.2 fill
当你需要一个所有元素都相同的固定长度数组时,fill
非常实用。
String[] stutter = new String[3];
Arrays.fill(stutter, "once");
assertTrue(Stream.of(stutter)
.allMatch(el -> "once".equals(el)));
📌 注意:必须先手动创建数组对象,再调用 fill
。不能像 Collections.nCopies()
那样直接生成。这是因为 Arrays.fill()
在泛型出现之前就已存在,设计上无法支持返回新数组。
💡 替代方案:若元素需不同,使用 setAll
(见 6.3 节)。
3. 数组比较
3.1 equals
与 deepEquals
Arrays.equals(arr1, arr2)
:仅比较一维数组的内容(长度 + 元素值)Arrays.deepEquals(arr1, arr2)
:递归比较多维或嵌套数组
✅ 示例:
assertTrue(
Arrays.equals(new String[] { "once", "upon", "a", "time" }, intro));
assertFalse(
Arrays.equals(new String[] { "once", "upon", "a", null }, intro));
Object[] story = new Object[] {
intro,
new String[] { "chapter one", "chapter two" },
new String[] { "the", "end" }
};
Object[] copy = new Object[] {
intro,
new String[] { "chapter one", "chapter two" },
new String[] { "the", "end" }
};
assertTrue(Arrays.deepEquals(story, copy));
assertFalse(Arrays.equals(story, copy)); // ❌ 引用不等
⚠️ 踩坑提醒:deepEquals
会递归调用自身,如果数组中存在自引用(循环引用),会导致栈溢出。生产环境务必注意数据结构设计。
3.2 hashCode
与 deepHashCode
对应 equals
/ deepEquals
,这两个方法用于生成数组的哈希值,遵守 Java 对象的 equals-hashCode
合约。
Object[] looping = new Object[]{ intro, intro };
int hashBefore = Arrays.hashCode(looping);
int deepHashBefore = Arrays.deepHashCode(looping);
修改 intro
内容后:
intro[3] = null;
int hashAfter = Arrays.hashCode(looping);
int deepHashAfter = Arrays.deepHashCode(looping);
assertEquals(hashAfter, hashBefore); // ✅ 外层引用未变
assertNotEquals(deepHashAfter, deepHashBefore); // ❌ 内容已变
📌 关键点:deepHashCode
会深入比较嵌套数组的内容,因此在将数组作为 HashMap
或 HashSet
的 key 时,必须使用 deepHashCode
才能保证行为正确。
4. 排序与查找
4.1 sort
对数组进行原地排序(⚠️ 会修改原数组)。
String[] sorted = Arrays.copyOf(intro, 4);
Arrays.sort(sorted);
assertArrayEquals(new String[]{ "a", "once", "time", "upon" }, sorted);
📌 实现细节:
- 基本类型数组:使用 双轴快排(dual-pivot quicksort)
- 对象数组:使用 Timsort(稳定排序)
两者平均时间复杂度均为 O(n log n)
。
💡 Java 8+ 提供 parallelSort
,利用 Fork/Join 框架实现并行排序,对大数组性能更优。
4.2 binarySearch
在已排序数组中执行二分查找,时间复杂度 O(log n)
。
int exact = Arrays.binarySearch(sorted, "time");
int caseInsensitive = Arrays.binarySearch(sorted, "TiMe", String::compareToIgnoreCase);
assertEquals("time", sorted[exact]);
assertEquals(2, exact);
assertEquals(exact, caseInsensitive);
⚠️ 踩坑重点:
数组必须事先排序,否则结果不可预测!
若未提供 Comparator
,则要求元素实现 Comparable
接口。
5. 流式处理(Streaming)
Java 8 引入 Stream 后,Arrays
类也新增了相关支持。
5.1 stream
将数组转为 Stream<T>
,便于链式操作。
assertEquals(4, Arrays.stream(intro).count());
// 指定范围:fromIndex(含)到 toIndex(不含)
exception.expect(ArrayIndexOutOfBoundsException.class);
Arrays.stream(intro, 2, 1).count(); // ❌ 范围非法
支持范围流:stream(array, from, to)
,但索引必须合法且 from <= to
,否则抛出 ArrayIndexOutOfBoundsException
。
6. 数组转换
6.1 toString
与 deepToString
快速获取数组的可读字符串表示。
assertEquals("[once, upon, a, time]", Arrays.toString(intro));
对于嵌套数组,必须使用 deepToString
:
assertEquals(
"[[once, upon, a, time], [chapter one, chapter two], [the, end]]",
Arrays.deepToString(story));
✅ 简单粗暴:调试时打印数组内容首选 deepToString
。
6.2 asList
将数组转为 List
,非常常用。
List<String> list = Arrays.asList(intro);
assertTrue(list.contains("upon"));
assertEquals(4, list.size());
⚠️ 踩坑警告:
- 返回的
List
是 固定长度,不支持add
/remove
- 其底层是
Arrays
的私有内部类ArrayList
(注意不是java.util.ArrayList
),仅实现了List
接口的部分类
📌 建议:若需可变列表,应进一步包装:
List<String> mutable = new ArrayList<>(Arrays.asList(arr));
6.3 setAll
通过函数式接口批量设置数组元素,参数为索引。
String[] longAgo = new String[4];
Arrays.setAll(longAgo, i -> getWord(i));
assertArrayEquals(longAgo, new String[]{"a","long","time","ago"});
⚠️ 注意:
若 lambda 抛出异常,数组的最终状态是未定义的(可能部分已修改),需谨慎处理异常。
7. 并行前缀计算(Parallel Prefix)
Java 8 引入的 parallelPrefix
方法,用于对数组执行并行的累积操作(如前缀和)。
7.1 parallelPrefix
int[] arr = { 1, 2, 3, 4 };
Arrays.parallelPrefix(arr, (left, right) -> left + right);
// 结果:[1, 3, 6, 10]
assertThat(arr, is(new int[] { 1, 3, 6, 10 }));
支持指定范围:
int[] arri = { 1, 2, 3, 4, 5 };
Arrays.parallelPrefix(arri, 1, 4, (left, right) -> left + right);
// 结果:[1, 2, 5, 9, 5]
assertThat(arri, is(new int[] { 1, 2, 5, 9, 5 }));
⚠️ 重要限制:
- 操作必须是 无副作用 且 满足结合律(associative)
- 否则并行计算可能导致结果不一致
反例(非结合律函数):
int nonassociativeFunc(int left, int right) {
return left + right * left;
}
测试证明结果不可靠:
@Test
public void whenPrefixNonAssociative_thenError() {
boolean consistent = true;
Random r = new Random();
for (int k = 0; k < 100_000; k++) {
int[] arrA = r.ints(100, 1, 5).toArray();
int[] arrB = Arrays.copyOf(arrA, arrA.length);
Arrays.parallelPrefix(arrA, this::nonassociativeFunc);
for (int i = 1; i < arrB.length; i++) {
arrB[i] = nonassociativeFunc(arrB[i - 1], arrB[i]);
}
consistent = Arrays.equals(arrA, arrB);
if (!consistent) break;
}
assertFalse(consistent);
}
7.2 性能对比
使用 JMH 在 6 核 Intel Xeon 机器上测试大数组前缀和:
Benchmark | Mode | Cnt | Score | Error | Units |
---|---|---|---|---|---|
largeArrayLoopSum | thrpt | 5 | 9.428 | ±0.075 | ops/s |
largeArrayParallelPrefixSum | thrpt | 5 | 15.235 | ±0.075 | ops/s |
largeArrayLoopSum | avgt | 5 | 105.825 | ±0.846 | ns/op |
largeArrayParallelPrefixSum | avgt | 5 | 65.676 | ±0.828 | ns/op |
✅ 结论:parallelPrefix
在大数组场景下显著优于传统循环,得益于并行计算。
基准代码:
@Benchmark
public void largeArrayLoopSum(BigArray bigArray, Blackhole blackhole) {
for (int i = 0; i < ARRAY_SIZE - 1; i++) {
bigArray.data[i + 1] += bigArray.data[i];
}
blackhole.consume(bigArray.data);
}
@Benchmark
public void largeArrayParallelPrefixSum(BigArray bigArray, Blackhole blackhole) {
Arrays.parallelPrefix(bigArray.data, (left, right) -> left + right);
blackhole.consume(bigArray.data);
}
8. 总结
java.util.Arrays
是 Java 开发中不可或缺的工具类。本文覆盖了其核心方法:
- ✅ 创建:
copyOf
,copyOfRange
,fill
,setAll
- ✅ 比较:
equals
/deepEquals
,hashCode
/deepHashCode
- ✅ 排序查找:
sort
,binarySearch
,parallelSort
- ✅ 转换:
toString
,asList
,stream
- ✅ 高级操作:
parallelPrefix
📌 特别提醒:
asList
返回的是固定长度列表,别踩坑binarySearch
前必须排序parallelPrefix
要求操作满足结合律setAll
和parallelPrefix
中的 lambda 异常需谨慎处理
该类在 Java 8(Stream 支持)和 Java 9(mismatch
方法)中持续增强,建议结合最新版本使用,充分发挥其能力。