1. 序章

このチュートリアルでは、 java .util.Arrays を見ていきます。これは、Java1.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. 塗りつぶし

別の方法として、固定長の配列を作成できます。これは、 fill、です。これは、すべての要素が同じである配列が必要な場合に役立ちます。

String[] stutter = new String[3];
Arrays.fill(stutter, "once");

assertTrue(Stream.of(stutter)
  .allMatch(el -> "once".equals(el));

setAll をチェックして、要素が異なる配列を作成します。

この機能が導入されたため、 String [] fill = 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));

deepE quals は合格しますが、に失敗します。

これは、deepEqualsが配列に遭遇するたびに最終的に自分自身を呼び出すのに対し、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は、配列でHashMapやHashSetなどのデータ構造を操作するときに使用される基本的な計算です。

4. 並べ替えと検索

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

4.1. ソート

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

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

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

ソートが元の参照を変更することに注意してください。これが、ここでコピーを実行する理由です。

sort は、配列要素タイプごとに異なるアルゴリズムを使用します。 プリミティブタイプはデュアルピボットクイックソートを使用し、オブジェクトタイプはティムソートを使用します。 どちらも、ランダムにソートされた配列の 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);

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

また、配列が最初にソートされていない場合、binarySearchは期待どおりに機能しないことに注意してください!

5. ストリーミング

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

5.1. ストリーム

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

Assert.assertEquals(Arrays.stream(intro).count(), 4);

exception.expect(ArrayIndexOutOfBoundsException.class);
Arrays.stream(intro, 2, 1).count();

ストリームに包括的および排他的なインデックスを提供できますが、インデックスが順不同、負、または範囲外の場合は、ArrayIndexOutOfBoundsExceptionを期待する必要があります。

6. 変身

最後に、 toString、 asList、、および setAll は、配列を変換するための2つの異なる方法を提供します。

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.  asList

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

List<String> rets = Arrays.asList(storyIntro);

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

ただし、返されるリストは固定長になるため、要素を追加または削除することはできません

不思議なことに、 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"});

そしてもちろん、例外処理はラムダを使用する上で最も厄介な部分の1つです。 したがって、ここで、ラムダが例外をスローした場合、Javaは配列の最終状態を定義しないことに注意してください。

7. パラレルプレフィックス

Java8以降に導入された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 }));

このメソッドは並列で実行されるため、累積演算は副作用がなく、連想的である必要があることに注意してください。

非結合関数の場合:

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を搭載した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 8 のストリーム生成および消費メソッドと、 Java 9 の不一致メソッドを含む、より最近のJavaリリースで拡張されました。

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