java.util.Arrays 类指南
最后更新:2025 年 4 月 4 日
1. 简介
在本教程中,我们将了解 java.util.Arrays,这是一个实用工具类,自 Java 1.2 起就包含在 Java 中。
使用 Arrays,我们可以创建、比较、排序、搜索、流式处理和转换数组。
2. 创建
让我们看看我们可以创建数组的一些方法:copyOf、copyOfRange 和 fill。
2.1. copyOf 和 copyOfRange
要使用 copyOfRange,我们需要原始数组以及我们想要复制的起始索引(包含)和结束索引(不包含)
String[] intro = new String[] { "once", "upon", "a", "time" };
String[] abridgement = Arrays.copyOfRange(storyIntro, 0, 3);
assertArrayEquals(new String[] { "once", "upon", "a" }, abridgement);
assertFalse(Arrays.equals(intro, abridgement));
要使用 copyOf,我们将采用intro和一个目标数组大小,并返回一个具有该长度的新数组
String[] revised = Arrays.copyOf(intro, 3);
String[] expanded = Arrays.copyOf(intro, 5);
assertArrayEquals(Arrays.copyOfRange(intro, 0, 3), revised);
assertNull(expanded[4]);
请注意,copyOf 如果我们的目标大小大于原始大小,则会用 null 填充数组。
2.2. fill
另一种方法是创建固定长度的数组,即 fill,当我们需要所有元素都相同的数组时很有用
String[] stutter = new String[3];
Arrays.fill(stutter, "once");
assertTrue(Stream.of(stutter)
.allMatch(el -> "once".equals(el));
查看 setAll 以创建元素不同的数组。
请注意,我们需要自己实例化数组——与String[] filled = Arrays.fill(“once”, 3); 之类的内容相反——因为此功能是在语言中提供泛型之前引入的。
3. 比较
现在让我们切换到比较数组的方法。
3.1. equals 和 deepEquals
我们可以使用 equals 进行简单的数组比较,按大小和内容进行比较。 如果我们将 null 作为其中一个元素添加,则内容检查将失败
assertTrue(
Arrays.equals(new String[] { "once", "upon", "a", "time" }, intro));
assertFalse(
Arrays.equals(new String[] { "once", "upon", "a", null }, intro));
当我们需要嵌套或多维数组时,可以使用 deepEquals,它不仅会检查顶层元素,还会递归地执行检查
Object[] story = new Object[]
{ intro, new String[] { "chapter one", "chapter two" }, end };
Object[] copy = new Object[]
{ intro, new String[] { "chapter one", "chapter two" }, end };
assertTrue(Arrays.deepEquals(story, copy));
assertFalse(Arrays.equals(story, copy));
请注意 deepEquals 通过,而 equals 失败。
这是因为 deepEquals 最终会在每次遇到数组时调用自身,而 equals 仅会比较子数组的引用。
此外,这使得在具有自引用数组上调用它变得危险!
3.2. hashCode 和 deepHashCode
hashCode 的实现将为我们提供 equals/hashCode 合同的另一部分,该合同推荐用于 Java 对象。 我们使用 hashCode 基于数组的内容计算整数
Object[] looping = new Object[]{ intro, intro };
int hashBefore = Arrays.hashCode(looping);
int deepHashBefore = Arrays.deepHashCode(looping);
现在,我们将原始数组的一个元素设置为 null 并重新计算哈希值
intro[3] = null;
int hashAfter = Arrays.hashCode(looping);
或者,deepHashCode 检查嵌套数组是否具有匹配的元素数量和内容。 如果我们使用 deepHashCode 重新计算
int deepHashAfter = Arrays.deepHashCode(looping);
现在,我们可以看到两种方法之间的差异
assertEquals(hashAfter, hashBefore);
assertNotEquals(deepHashAfter, deepHashBefore);
deepHashCode 是我们在处理数据结构(如 HashMap 和 HashSet)的数组时使用的底层计算。.
4. 排序和搜索
接下来,让我们看看数组的排序和搜索。
4.1. sort
如果我们的元素是基本类型或者它们实现了Comparable接口,我们可以使用 sort 来进行原地排序
String[] sorted = Arrays.copyOf(intro, 4);
Arrays.sort(sorted);
assertArrayEquals(
new String[]{ "a", "once", "time", "upon" },
sorted);
请注意,sort会修改原始引用,这就是为什么我们在这里进行复制。
sort 会为不同的数组元素类型使用不同的算法。 基本类型使用双枢轴快速排序,而 对象类型使用Timsort。两者对于随机排序的数组而言,平均情况下的时间复杂度都是 O(n log(n)) 。
从Java 8开始, parallelSort 可用于并行排序合并。 它提供了一种并发排序方法,使用多个 Arrays.sort 任务。
4.2. binarySearch
在未排序的数组中搜索是线性的,但如果我们有一个排序后的数组,那么我们可以在 O(log n) 的时间内完成,这正是 binarySearch所能做到的:
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作为第三个参数,那么 binarySearch会依赖于我们的元素类型是 Comparable类型。
再次提醒,如果我们的数组没有首先排序,那么 binarySearch 将无法按预期工作!
5. 流处理
正如我们之前看到的, Arrays 在Java 8中得到了更新,包括使用Stream API的方法,例如 parallelSort(如上所述)、 stream 和 setAll。
5.1. stream
stream为我们的数组提供了完全访问Stream API的权限
Assert.assertEquals(Arrays.stream(intro).count(), 4);
exception.expect(ArrayIndexOutOfBoundsException.class);
Arrays.stream(intro, 2, 1).count();
我们可以提供包含和排除索引给流,但是如果索引顺序错误、为负数或超出范围,我们应该期望出现 ArrayIndexOutOfBoundsException。
6. 转换
最后, toString, asList, 和 setAll 为我们提供了几种不同的数组转换方式。
6.1. toString 和 deepToString
我们可以使用 toString获得原始数组的可读版本:
assertEquals("[once, upon, a, time]", Arrays.toString(storyIntro));
再次强调,我们必须使用 deep 版本来打印嵌套数组的内容
assertEquals(
"[[once, upon, a, time], [chapter one, chapter two], [the, end]]",
Arrays.deepToString(story));
6.2. asList
Arrays方法中最方便我们使用的方法是 asList。我们有一种将数组转换为列表的简单方法
List<String> rets = Arrays.asList(storyIntro);
assertTrue(rets.contains("upon"));
assertTrue(rets.contains("time"));
assertEquals(rets.size(), 4);
但是,返回的 List将是固定长度的,因此我们无法添加或删除元素。
请注意,有趣的是,java.util.Arrays拥有自己的 ArrayList子类,而 asList 返回的就是这个子类。在调试时这可能会非常具有迷惑性!
6.3. setAll
使用 setAll,我们可以使用函数式接口设置数组的所有元素。生成器实现将位置索引作为参数
String[] longAgo = new String[4];
Arrays.setAll(longAgo, i -> this.getWord(i));
assertArrayEquals(longAgo, new String[]{"a","long","time","ago"});
而且,当然,异常处理是使用lambda表达式中更棘手的部分之一。所以请记住,如果lambda抛出异常,Java不会定义数组的最终状态。
7. 并行前缀
自Java 8以来,Arrays中的另一个新方法是 parallelPrefix。 使用 parallelPrefix,我们可以以累积方式操作输入数组的每个元素。
7.1. parallelPrefix
如果运算符执行加法,如下例所示,[1, 2, 3, 4] 将得到[1, 3, 6, 10]:
int[] arr = new int[] { 1, 2, 3, 4};
Arrays.parallelPrefix(arr, (left, right) -> left + right);
assertThat(arr, is(new int[] { 1, 3, 6, 10}));
此外,我们可以为该操作指定一个子范围
int[] arri = new int[] { 1, 2, 3, 4, 5 };
Arrays.parallelPrefix(arri, 1, 4, (left, right) -> left + right);
assertThat(arri, is(new int[] { 1, 2, 5, 9, 5 }));
请注意,该方法是并行执行的,因此累积运算应该是无副作用的且具有结合性。
对于非结合函数
int nonassociativeFunc(int left, int right) {
return left + right*left;
}
使用 parallelPrefix会产生不一致的结果
@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 在 Intel Xeon 机器(6 核)上运行微基准测试时,我们可以看到显著的性能提升。
Benchmark Mode Cnt Score Error Units
largeArrayLoopSum thrpt 5 9.428 ± 0.075 ops/s
largeArrayParallelPrefixSum thrpt 5 15.235 ± 0.075 ops/s
Benchmark Mode Cnt Score Error Units
largeArrayLoopSum avgt 5 105.825 ± 0.846 ops/s
largeArrayParallelPrefixSum avgt 5 65.676 ± 0.828 ops/s
以下是基准测试代码
@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);
}
7. 结论
在本文中,我们学习了如何使用 java.util.Arrays 类创建、搜索、排序和转换数组的一些方法。
该类在较新的 Java 版本中得到了扩展,包含了 Java 8 中流生成和消费方法(在 Java 8 中)和 Java 9 中的不匹配方法(在 Java 9 中)。
支持本文的代码可在 GitHub 上获取。 一旦你以 Baeldung Pro 会员 身份登录,就开始学习并在项目上进行编码。















