java.util.Arraysクラスの手引き

java.util.Arraysクラスのガイド

1. 前書き

このチュートリアルでは、Java 1.2以降Javaの一部となっているユーティリティクラスであるjava.util.Arraysを見ていきます。

Arrays, weを使用すると、配列を作成、比較、並べ替え、検索、ストリーミング、変換できます。

2. 作成

配列を作成する方法のいくつかを見てみましょう:copyOfcopyOfRange、および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 pads the array with nulls if our target size is bigger than the original size.に注意してください

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を使用できます。 要素の1つとして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 は不合格になることに注意してください_._

This is because deepEquals ultimately calls itself each time it encounters an 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);

これで、2つの方法の違いがわかります。

assertEquals(hashAfter, hashBefore);
assertNotEquals(deepHashAfter, deepHashBefore);

deepHashCode is the underlying calculation used when we are working with data structures like HashMap and HashSet on arrays

4. 並べ替えと検索

次に、配列の並べ替えと検索を見てみましょう。

4.1. sort

要素がプリミティブであるか、Comparableを実装している場合は、sort を使用してインラインソートを実行できます。

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

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

Take care that sort mutates the original reference、これがここでコピーを実行する理由です。

sort は、配列要素タイプごとに異なるアルゴリズムを使用します。 Primitive types use a dual-pivot quicksortおよびObject types use Timsort。 どちらも、ランダムに並べ替えられた配列の平均ケースはO(n log(n)) です。

Java 8以降、parallelSort isは並列ソートマージで使用できます。 複数の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);

3番目のパラメータとしてComparatorを指定しない場合、binarySearchは要素タイプがタイプComparableであると見なします。

また、if our array isn’t first sorted, then binarySearch won’t work as we expect!に注意してください

5. ストリーミング

前に見たように、Arrays はJava 8で更新され、parallelSort(上記)、stream setAll.などのStreamAPIを使用するメソッドが含まれるようになりました。

5.1. stream

streamは、配列のStreamAPIへのフルアクセスを提供します。

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));

再びwe must use the deep version to print the contents of nested arrays

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

6.2. asList

使用するすべてのArraysメソッドの中で最も便利なのは、asList.です。配列をリストに変換する簡単な方法があります。

List rets = Arrays.asList(storyIntro);

assertTrue(rets.contains("upon"));
assertTrue(rets.contains("time"));
assertEquals(rets.size(), 4);

ただし、the returned List will be a fixed length so we won’t be able to add or remove elements

不思議なことに、java.util.Arrays has its own ArrayList subclass, which asList returnsにも注意してください。 これはデバッグ時に非常に誤解を招く可能性があります!

6.3. setAll

setAllを使用すると、機能インターフェイスを使用して配列のすべての要素を設定できます。 ジェネレーターの実装は、位置インデックスをパラメーターとして受け取ります。

String[] longAgo = new String[4];
Arrays.setAll(longAgo, i -> this.getWord(i));
assertArrayEquals(longAgo, new String[]{"a","long","time","ago"});

そして、もちろん、例外処理は、ラムダを使用する際の危険な部分の1つです。 ここで、if the lambda throws an exception, then Java doesn’t define the final state of the array.を覚えておいてください

7. 並列プレフィックス

Java 8以降に導入されたArraysのもう1つの新しいメソッドは、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 }));

このメソッドは並列で実行されるため、the cumulative operation should be side-effect-free and associativeであることに注意してください。

非連想関数の場合:

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 9に不一致メソッドが含まれています。

この記事のソースは、いつものように、over on Githubです。