Javaでのストリームの順序付け

Javaでのストリームの順序付け

1. 概要

このチュートリアルでは、different uses of the Java Stream API affect the order in which a stream generates, processes, and collects dataについて詳しく説明します。

また、ordering influences performanceについても見ていきます。

2. 遭遇注文

簡単に言えば、encounter order the order in which a Stream encounters dataです。

2.1. Collectionソースの遭遇順序

ソースとして選択したCollectionは、Stream.の遭遇順序に影響を与える。

これをテストするために、2つのストリームを作成してみましょう。

1つ目は、固有の順序を持​​つListから作成されます。

2つ目は、TreeSetから作成されたものです。

次に、各Streamの出力をArrayに収集して、結果を比較します。

@Test
public void givenTwoCollections_whenStreamedSequentially_thenCheckOutputDifferent() {
    List list = Arrays.asList("B", "A", "C", "D", "F");
    Set set = new TreeSet<>(list);

    Object[] listOutput = list.stream().toArray();
    Object[] setOutput = set.stream().toArray();

    assertEquals("[B, A, C, D, F]", Arrays.toString(listOutput));
    assertEquals("[A, B, C, D, F]", Arrays.toString(setOutput));
}

この例からわかるように、TreeSet は入力シーケンスの順序を維持していないため、Streamの遭遇順序をスクランブルします。

Streamが順序付けられている場合、実装はStreamの遭遇順序を維持します。

並列ストリームを使用してテストを繰り返すと、同じ結果が得られます。

@Test
public void givenTwoCollections_whenStreamedInParallel_thenCheckOutputDifferent() {
    List list = Arrays.asList("B", "A", "C", "D", "F");
    Set set = new TreeSet<>(list);

    Object[] listOutput = list.stream().parallel().toArray();
    Object[] setOutput = set.stream().parallel().toArray();

    assertEquals("[B, A, C, D, F]", Arrays.toString(listOutput));
    assertEquals("[A, B, C, D, F]", Arrays.toString(setOutput));
}

2.2. 注文の削除

いつでも、explicitly remove the order constraint with the unordered methodできます。

たとえば、TreeSetを宣言しましょう。

Set set = new TreeSet<>(
  Arrays.asList(-9, -5, -4, -2, 1, 2, 4, 5, 7, 9, 12, 13, 16, 29, 23, 34, 57, 102, 230));

また、unorderedを呼び出さずにストリーミングする場合:

set.stream().parallel().limit(5).toArray();

次に、TreeSetの自然な順序が保持されます。

[-9, -5, -4, -2, 1]

ただし、順序を明示的に削除する場合:

set.stream().unordered().parallel().limit(5).toArray();

その場合、出力は異なります。

[1, 4, 7, 9, 23]

その理由は2つあります。1つは、シーケンシャルストリームがデータを一度に1要素ずつ処理するため、unordered 自体はほとんど影響を与えません。 ただし、parallelを呼び出すと、出力に影響しました。

3. 中間操作

affect stream ordering through intermediate operationsも可能です。

ほとんどの中間操作はStream,の順序を維持しますが、その性質上、変更するものもあります。

たとえば、ソートによってストリームの順序に影響を与えることができます。

@Test
public void givenUnsortedStreamInput_whenStreamSorted_thenCheckOrderChanged() {
    List list = Arrays.asList(-3, 10, -4, 1, 3);

    Object[] listOutput = list.stream().toArray();
    Object[] listOutputSorted = list.stream().sorted().toArray();

    assertEquals("[-3, 10, -4, 1, 3]", Arrays.toString(listOutput));
    assertEquals("[-4, -3, 1, 3, 10]", Arrays.toString(listOutputSorted));
}

unordered emptyは、最終的にStream.の順序を変更する中間操作のもう2つの例です。

4. ターミナル運営

最後に、順序depending on the terminal operation that we useに影響を与えることができます。

4.1. ForEach vsForEachOrdered

ForEach ForEachOrderedは同じ機能を提供しているように見えるかもしれませんが、1つの重要な違いがあります:ForEachOrdered guarantees to maintain the order of the Stream.

リストを宣言する場合:

List list = Arrays.asList("B", "A", "C", "D", "F");

並列化した後、forEachOrderedを使用します。

list.stream().parallel().forEachOrdered(e -> logger.log(Level.INFO, e));

次に、出力が順序付けられます。

INFO: B
INFO: A
INFO: C
INFO: D
INFO: F

ただし、forEach:を使用する場合

list.stream().parallel().forEach(e -> logger.log(Level.INFO, e));

その場合、出力はunorderedです。

INFO: C
INFO: F
INFO: B
INFO: D
INFO: A

ForEachは、各スレッドから到着した順序で要素をログに記録します。 2番目のStream は、log メソッドを呼び出す前にそのForEachOrdered method waits for each previous thread to completeと一緒になります。

4.2. Collect

collect メソッドを使用してStream outputを集計する場合、選択したCollectionが順序に影響することに注意することが重要です。

たとえば、inherently unordered Collections such as TreeSet won’t obey the order of the Streamの出力は次のとおりです。

@Test
public void givenSameCollection_whenStreamCollected_checkOutput() {
    List list = Arrays.asList("B", "A", "C", "D", "F");

    List collectionList = list.stream().parallel().collect(Collectors.toList());
    Set collectionSet = list.stream().parallel()
      .collect(Collectors.toCollection(TreeSet::new));

    assertEquals("[B, A, C, D, F]", collectionList.toString());
    assertEquals("[A, B, C, D, F]", collectionSet.toString());
}

コードを実行すると、Set.に収集することで、Streamの順序が変わることがわかります。

4.3. Collectionsの指定

たとえば、Collectors.toMapを使用して順序付けされていないコレクションに収集する場合でも、changing the implementation of our Collectors methods to use the Linked implementationによる順序付けを強制できます。

まず、toMapメソッドの通常の2-parameter versionとともに、リストを初期化します。

@Test
public void givenList_whenStreamCollectedToHashMap_thenCheckOrderChanged() {
  List list = Arrays.asList("A", "BB", "CCC");

  Map hashMap = list.stream().collect(Collectors
    .toMap(Function.identity(), String::length));

  Object[] keySet = hashMap.keySet().toArray();

  assertEquals("[BB, A, CCC]", Arrays.toString(keySet));
}

予想どおり、新しいHashMapは入力リストの元の順序を保持していませんが、それを変更しましょう。

2番目のStreamでは、toMap メソッドの4-parameter versionを使用して、supplier に新しいLinkedHashMapを提供するように指示します。

@Test
public void givenList_whenCollectedtoLinkedHashMap_thenCheckOrderMaintained(){
    List list = Arrays.asList("A", "BB", "CCC");

    Map linkedHashMap = list.stream().collect(Collectors.toMap(
      Function.identity(),
      String::length,
      (u, v) -> u,
      LinkedHashMap::new
    ));

    Object[] keySet = linkedHashMap.keySet().toArray();

    assertEquals("[A, BB, CCC]", Arrays.toString(keySet));
}

ねえ、それははるかに良いです!

データをLinkedHashMapに収集することで、リストの元の順序を維持することができました。

5. パフォーマンス

シーケンシャルストリームを使用している場合、順序の有無はプログラムのパフォーマンスにほとんど影響しません。 Parallel streams, however, can be heavily affected by the presence of an ordered Stream.

これは、各スレッドがStreamの前の要素の計算を待機する必要があるためです。

パフォーマンスを測定するために、Java Microbenchmark harness、JMHを使用してこれを実証してみましょう。

次の例では、いくつかの一般的な中間操作を使用して、順序付きおよび順序なしの並列ストリームを処理するパフォーマンスコストを測定します。

5.1. Distinct

順序付けされたストリームと順序付けられていないストリームの両方でdistinct 関数を使用してテストを設定しましょう。

@Benchmark
public void givenOrderedStreamInput_whenStreamDistinct_thenShowOpsPerMS() {
    IntStream.range(1, 1_000_000).parallel().distinct().toArray();
}

@Benchmark
public void givenUnorderedStreamInput_whenStreamDistinct_thenShowOpsPerMS() {
    IntStream.range(1, 1_000_000).unordered().parallel().distinct().toArray();
}

runをヒットすると、操作ごとにかかる時間の不一致を確認できます。

Benchmark                        Mode  Cnt       Score   Error  Units
TestBenchmark.givenOrdered...    avgt    2  222252.283          us/op
TestBenchmark.givenUnordered...  avgt    2   78221.357          us/op

5.2. Filter 

次に、並列のStreamと単純なfilter メソッドを使用して、10番目ごとの整数:を返します。

@Benchmark
public void givenOrderedStreamInput_whenStreamFiltered_thenShowOpsPerMS() {
    IntStream.range(1, 100_000_000).parallel().filter(i -> i % 10 == 0).toArray();
}

@Benchmark
public void givenUnorderedStreamInput_whenStreamFiltered_thenShowOpsPerMS(){
    IntStream.range(1,100_000_000).unordered().parallel().filter(i -> i % 10 == 0).toArray();
}

興味深いことに、2つのストリームの違いは、distinct メソッドを使用する場合よりもはるかに小さくなっています。

Benchmark                        Mode  Cnt       Score   Error  Units
TestBenchmark.givenOrdered...    avgt    2  116333.431          us/op
TestBenchmark.givenUnordered...  avgt    2  111471.676          us/op

6. 結論

この記事では、the different stages of the Stream process and how each one has its own effectに焦点を当てて、ストリームの 順序を確認しました。

最後に、order contract placed on a Stream can affect the performance of parallel streams.がどのように

いつものように、完全なサンプルセットover on GitHubを確認してください。