1. 简介

java.util.Arrays 是自 Java 1.2 起就存在的工具类,属于 Java 核心类库中的常青树。它提供了创建、比较、排序、查找、流式处理和转换数组的一系列静态方法。

对于有经验的开发者来说,Arrays 类几乎是日常编码中绕不开的工具。本文将带你系统梳理其核心能力,避免踩坑,提升编码效率。


2. 数组创建

2.1 copyOfcopyOfRange

这两个方法用于从已有数组复制出新数组,区别在于:

  • 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(引用类型)或默认值(如 0false)填充


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 equalsdeepEquals

  • 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 hashCodedeepHashCode

对应 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 会深入比较嵌套数组的内容,因此在将数组作为 HashMapHashSet 的 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 toStringdeepToString

快速获取数组的可读字符串表示。

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 要求操作满足结合律
  • setAllparallelPrefix 中的 lambda 异常需谨慎处理

该类在 Java 8(Stream 支持)和 Java 9(mismatch 方法)中持续增强,建议结合最新版本使用,充分发挥其能力。


« 上一篇: 学习JUnit