1. 概要

このチュートリアルでは、 Java Stream APIのさまざまな使用法が、ストリームがデータを生成、処理、および収集する順序にどのように影響するかについて詳しく説明します。

また、の順序がパフォーマンスにどのように影響するかについても見ていきます。

2. 遭遇注文

簡単に言えば、遭遇順序ストリームがデータに遭遇する順序です。

2.1. コレクションソースの遭遇順序

ソースとして選択したコレクションは、ストリームの遭遇順序に影響します。

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

最初のものはリストから作成されます。これには固有の順序があります。

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

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

@Test
public void givenTwoCollections_whenStreamedSequentially_thenCheckOutputDifferent() {
    List<String> list = Arrays.asList("B", "A", "C", "D", "F");
    Set<String> 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<String> list = Arrays.asList("B", "A", "C", "D", "F");
    Set<String> 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. 注文の削除

いつでも、unorderedメソッドを使用して明示的に順序制約を削除できます。

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

Set<Integer> 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つの要素でデータを処理するため、順序付けされていない自体はほとんど効果がありません。 ただし、 parallel を呼び出すと、出力に影響しました。

3. 中間操作

また、中間操作を介して河川次数に影響を与えることができます。

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

たとえば、次のように並べ替えることで、ストリームの順序に影響を与えることができます。

@Test
public void givenUnsortedStreamInput_whenStreamSorted_thenCheckOrderChanged() {
    List<Integer> 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));
}

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

4. ターミナルオペレーション

最後に、使用する端末操作に応じて、順序に影響を与えることができます。

4.1.  ForEach vs ForEachOrdered

ForEachForEachOrderedは同じ機能を提供しているように見えますが、重要な違いが1つあります。ForEachOrderedはストリームの順序を維持することを保証します。

リストを宣言する場合:

List<String> 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 は、各スレッドから到着した順序で要素をログに記録します。 ForEachOrderedメソッドを持つ2番目のストリームは、ログメソッドを呼び出す前に、前の各スレッドが完了するを待機します。

4.2. 収集

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

たとえば、TreeSetなどの本質的に順序付けされていないコレクションは、Stream出力の順序に従いません。

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

    List<String> collectionList = list.stream().parallel().collect(Collectors.toList());
    Set<String> 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()); 
}

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

4.3. コレクションの指定

たとえば、 Collectors.toMap を使用して順序付けされていないコレクションに収集する場合でも、リンクされた実装を使用するようにCollectorsメソッドの実装を変更することで、順序付けを強制できます。

まず、 toMapメソッドの通常の2パラメーターバージョンとともに、リストを初期化します。

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

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

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

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

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

2番目のStreamでは、[X43X] toMapメソッドの4パラメーターバージョンを使用して、サプライヤーに新しい LinkedHashMap

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

    Map<String, Integer> 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. パフォーマンス

シーケンシャルストリームを使用している場合、順序の有無によってプログラムのパフォーマンスにほとんど違いはありません。 ただし、並列ストリームは、順序付けられたストリームの存在によって大きな影響を受ける可能性があります。

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

Javaマイクロベンチマークハーネス、JMHを使用してこれを実証し、パフォーマンスを測定してみましょう。

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

5.1. 個別

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

@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. フィルター

次に、並列 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つのストリームの違いは、個別のメソッドを使用する場合よりもはるかに小さくなります。

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

6. 結論

この記事では、 ストリームの順序付けについて、ストリームプロセスのさまざまな段階と、それぞれが独自の効果を発揮する方法に焦点を当てて説明しました。

最後に、ストリームに設定された注文契約が並列ストリームのパフォーマンスにどのように影響するかを確認しました。

いつものように、GitHubで完全なサンプルセットをチェックしてください。