java.util.Arrays类指南

2023/06/09

1. 概述

在本教程中,我们将了解java.util.Arrays,这是一个实用程序类,自Java 1.2以来一直是Java的一部分。

使用数组,我们可以创建、比较、排序、搜索、流式传输和转换数组。

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,我们将引入一个目标数组大小,然后我们将返回一个该长度的新数组:

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 充满

另一种方法,我们可以创建一个固定长度的数组,是填充,当我们想要一个所有元素都相同的数组时,这很有用:

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 等于和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最终会在每次遇到array时调用自己,而equals只会比较子数组的引用。

此外,这使得调用带有自引用的数组变得危险!

3.2 hashCode和deepHashCode

hashCode的实现将为我们提供推荐用于Java对象的equals/hashCode契约的另一部分。我们使用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 种类

如果我们的元素是原始元素或者它们实现了Comparable,我们可以使用sort来执行内联排序:

String[] sorted = Arrays.copyOf(intro, 4);
Arrays.sort(sorted);

assertArrayEquals(
  new String[]{ "a", "once", "time", "upon" }, 
  sorted);

注意sort会改变原始引用,这就是我们在这里执行的原因。

sort将对不同的数组元素类型使用不同的算法。原始类型使用双枢轴快速排序对象类型使用Timsort。对于随机排序的数组,两者都具有O(nlog(n))的平均情况。

从Java 8开始,parallelSort可用于并行排序合并。它提供了一种使用多个Arrays.sort任务的并发排序方法。

4.2 二分查找

在未排序的数组中搜索是线性的,但如果我们有一个排序的数组,那么我们可以在O(logn)中进行搜索,这就是我们可以使用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 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));

我们必须再次使用深层版本来打印嵌套数组的内容:

assertEquals(
  "[[once, upon, a, time], [chapter one, chapter two], [the, end]]",
  Arrays.deepToString(story));

6.2 作为列表

在所有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,我们可以使用函数式接口设置数组的所有元素。生成器实现将位置索引作为参数:

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 并行前缀

如果运算符执行如下示例中的加法,[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 + rightleft;
}

使用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的IntelXeon机器(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 9中包含了不匹配方法。

与往常一样,本教程的完整源代码可在GitHub上获得。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章