1. 概要

このチュートリアルでは、いくつかの補完的な機能を追加することで標準の StreamAPIを拡張するライブラリであるProtonpackの主な機能を見ていきます。

Java Stream APIの基礎については、この記事を参照してください。

2. Mavenの依存関係

Protonpackライブラリを使用するには、pom.xmlファイルに依存関係を追加する必要があります。

<dependency>
    <groupId>com.codepoetics</groupId>
    <artifactId>protonpack</artifactId>
    <version>1.15</version>
</dependency>

MavenCentralで最新バージョンを確認してください。

3. StreamUtils

これは、Javaの標準 StreamAPIを拡張するメインクラスです。

ここで説明するすべてのメソッドは中間操作です。つまり、ストリームを変更しますが、その処理はトリガーされません。

3.1. takeWhile()および takeUntil()

takeWhile()は、指定された条件を満たしている限り、ソースストリームから値を取得します。

Stream<Integer> streamOfInt = Stream
  .iterate(1, i -> i + 1);
List<Integer> result = StreamUtils
  .takeWhile(streamOfInt, i -> i < 5)
  .collect(Collectors.toList());
assertThat(result).contains(1, 2, 3, 4);

逆に、 takeUntil()は、値が指定された条件を満たすまで、値を取り、その後停止します。

Stream<Integer> streamOfInt = Stream
  .iterate(1, i -> i + 1);
List<Integer> result = StreamUtils
  .takeUntil(streamOfInt, i -> i >= 5)
  .collect(Collectors.toList());
assertThat(result).containsExactly(1, 2, 3, 4);

Java 9以降では、 takeWhile()は標準の StreamAPIの一部です。

3.2. zip()

zip()は、入力およびコンバイナー関数として2つまたは3つのストリームを受け取ります。 メソッドは、各ストリームの同じ位置から値を取得し、それをコンバイナーに渡します。

ストリームの1つが値を使い果たすまで、これを行います。

String[] clubs = {"Juventus", "Barcelona", "Liverpool", "PSG"};
String[] players = {"Ronaldo", "Messi", "Salah"};
Set<String> zippedFrom2Sources = StreamUtils
  .zip(stream(clubs), stream(players), (club, player) -> club + " " + player)
  .collect(Collectors.toSet());
 
assertThat(zippedFrom2Sources)
  .contains("Juventus Ronaldo", "Barcelona Messi", "Liverpool Salah");

同様に、3つのソースストリームを受け取るオーバーロードされた zip()

String[] leagues = { "Serie A", "La Liga", "Premier League" };
Set<String> zippedFrom3Sources = StreamUtils
  .zip(stream(clubs), stream(players), stream(leagues), 
    (club, player, league) -> club + " " + player + " " + league)
  .collect(Collectors.toSet());
 
assertThat(zippedFrom3Sources).contains(
  "Juventus Ronaldo Serie A", 
  "Barcelona Messi La Liga", 
  "Liverpool Salah Premier League");

3.3.  zipWithIndex()

zipWithIndex() 値を取得し、各値をそのインデックスでzipして、インデックス付きの値のストリームを作成します。

Stream<String> streamOfClubs = Stream
  .of("Juventus", "Barcelona", "Liverpool");
Set<Indexed<String>> zipsWithIndex = StreamUtils
  .zipWithIndex(streamOfClubs)
  .collect(Collectors.toSet());
assertThat(zipsWithIndex)
  .contains(Indexed.index(0, "Juventus"), Indexed.index(1, "Barcelona"), 
    Indexed.index(2, "Liverpool"));

3.4. merge()

merge()は、複数のソースストリームとコンバイナーで機能します。 は、各ソースストリームから同じインデックス位置の値を取得し、それをコンバイナに渡します。

このメソッドは、 seed 値から開始して、各ストリームの同じインデックスから1つの値を連続して取得することで機能します。

次に、値がコンバイナーに渡され、結果の結合された値がコンバイナーにフィードバックされて、次の値が作成されます。

Stream<String> streamOfClubs = Stream
  .of("Juventus", "Barcelona", "Liverpool", "PSG");
Stream<String> streamOfPlayers = Stream
  .of("Ronaldo", "Messi", "Salah");
Stream<String> streamOfLeagues = Stream
  .of("Serie A", "La Liga", "Premier League");

Set<String> merged = StreamUtils.merge(
  () ->  "",
  (valOne, valTwo) -> valOne + " " + valTwo,
  streamOfClubs,
  streamOfPlayers,
  streamOfLeagues)
  .collect(Collectors.toSet());

assertThat(merged)
  .contains("Juventus Ronaldo Serie A", "Barcelona Messi La Liga", 
    "Liverpool Salah Premier League", "PSG");

3.5.  mergeToList()

mergeToList()は、入力として複数のストリームを受け取ります。 は、各ストリームの同じインデックスの値をリストに結合します。

Stream<String> streamOfClubs = Stream
  .of("Juventus", "Barcelona", "PSG");
Stream<String> streamOfPlayers = Stream
  .of("Ronaldo", "Messi");

Stream<List<String>> mergedStreamOfList = StreamUtils
  .mergeToList(streamOfClubs, streamOfPlayers);
List<List<String>> mergedListOfList = mergedStreamOfList
  .collect(Collectors.toList());

assertThat(mergedListOfList.get(0))
  .containsExactly("Juventus", "Ronaldo");
assertThat(mergedListOfList.get(1))
  .containsExactly("Barcelona", "Messi");
assertThat(mergedListOfList.get(2))
  .containsExactly("PSG");

3.6.  interleave()

interleave() は、セレクターを使用して複数のストリームから取得した代替値を作成します。

このメソッドは、各ストリームからセレクターに1つの値を含むセットを提供し、セレクターは1つの値を選択します。

次に、選択した値がセットから削除され、選択した値が発生した次の値に置き換えられます。 この反復は、すべてのソースの値がなくなるまで続きます。

次の例では、 interleave()を使用して、ラウンドロビン戦略で交互の値を作成します。

Stream<String> streamOfClubs = Stream
  .of("Juventus", "Barcelona", "Liverpool");
Stream<String> streamOfPlayers = Stream
  .of("Ronaldo", "Messi");
Stream<String> streamOfLeagues = Stream
  .of("Serie A", "La Liga");

List<String> interleavedList = StreamUtils
  .interleave(Selectors.roundRobin(), streamOfClubs, streamOfPlayers, streamOfLeagues)
  .collect(Collectors.toList());
  
assertThat(interleavedList)
  .hasSize(7)
  .containsExactly("Juventus", "Ronaldo", "Serie A", "Barcelona", "Messi", "La Liga", "Liverpool");

ラウンドロビンセレクターはライブラリによってSelectors.roundRobin()として提供されているため、上記のコードはチュートリアル用であることに注意してください。

3.7.  skipUntil()および skipWhile()

skipUntil()は、値が条件を満たすまで、値をスキップします。

Integer[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
List skippedUntilGreaterThan5 = StreamUtils
  .skipUntil(stream(numbers), i -> i > 5)
  .collect(Collectors.toList());
 
assertThat(skippedUntilGreaterThan5).containsExactly(6, 7, 8, 9, 10);

対照的に、 skipWhile() は、値が条件を満たしている間、値をスキップします。

Integer[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
List skippedWhileLessThanEquals5 = StreamUtils
  .skipWhile(stream(numbers), i -> i <= 5 || )
  .collect(Collectors.toList());
 
assertThat(skippedWhileLessThanEquals5).containsExactly(6, 7, 8, 9, 10);

skipWhile()に関する重要な点の1つは、条件を満たさない最初の値が見つかった後もストリーミングを続行することです。

List skippedWhileGreaterThan5 = StreamUtils
  .skipWhile(stream(numbers), i -> i > 5)
  .collect(Collectors.toList());
assertThat(skippedWhileGreaterThan5).containsExactly(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

Java 9以降では、標準の Stream APIのdropWhile()は、 skipWhile()と同じ機能を提供します。

3.8.  unfold()

unfold()は、カスタムジェネレーターをシード値に適用し、次に生成された各値に適用することで、潜在的に無限のストリームを生成します。ストリームは、Optional.empty():を返すことで終了できます。

Stream<Integer> unfolded = StreamUtils
  .unfold(2, i -> (i < 100) 
    ? Optional.of(i * i) : Optional.empty());

assertThat(unfolded.collect(Collectors.toList()))
  .containsExactly(2, 4, 16, 256);

3.9. windowed()

windowed() ソースストリームの複数のサブセットをのストリームとして作成しますリスト。 このメソッドはソースストリームを取ります。 ウィンドウサイズ値をスキップパラメータとして。

Listの長さはwindow size、に等しく、s kip value は、前のサブセットに対してサブセットがどこから始まるかを決定します。

Integer[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8 };

List<List> windowedWithSkip1 = StreamUtils
  .windowed(stream(numbers), 3, 1)
  .collect(Collectors.toList());
assertThat(windowedWithSkip1)
  .containsExactly(asList(1, 2, 3), asList(2, 3, 4), asList(3, 4, 5), asList(4, 5, 6), asList(5, 6, 7));

さらに、次の例に示すように、最後のウィンドウは目的のサイズであることが保証されています。

List<List> windowedWithSkip2 = StreamUtils.windowed(stream(numbers), 3, 2).collect(Collectors.toList());
assertThat(windowedWithSkip2).containsExactly(asList(1, 2, 3), asList(3, 4, 5), asList(5, 6, 7));

3.10. 集計()

まったく異なる動作をする2つのaggregate()メソッドがあります。

最初のaggregate() は、指定された述語に従って等しい値の要素をグループ化します。

Integer[] numbers = { 1, 2, 2, 3, 4, 4, 4, 5 };
List<List> aggregated = StreamUtils
  .aggregate(Arrays.stream(numbers), (int1, int2) -> int1.compareTo(int2) == 0)
  .collect(Collectors.toList());
assertThat(aggregated).containsExactly(asList(1), asList(2, 2), asList(3), asList(4, 4, 4), asList(5));

述部は、連続した方法で値を受け取ります。 したがって、番号が注文されていない場合、上記は異なる結果になります。

一方、2番目の Aggregate()は、ソースストリームの要素を目的のサイズのグループにグループ化するために使用されます。

List<List> aggregatedFixSize = StreamUtils
  .aggregate(stream(numbers), 5)
  .collect(Collectors.toList());
assertThat(aggregatedFixSize).containsExactly(asList(1, 2, 2, 3, 4), asList(4, 4, 5));

3.11.  AggregateOnListCondition()

AggregateOnListCondition()は、述語と現在のアクティブグループに基づいて値をグループ化します。 述部には、現在アクティブなグループがListおよび次の値として指定されます。 次に、グループを続行するか、新しいグループを開始するかを決定する必要があります。

次の例は、連続する整数値をグループにグループ化する要件を解決します。各グループの値の合計は5を超えてはなりません。

Integer[] numbers = { 1, 1, 2, 3, 4, 4, 5 };
Stream<List<Integer>> aggregated = StreamUtils
  .aggregateOnListCondition(stream(numbers), 
    (currentList, nextInt) -> currentList.stream().mapToInt(Integer::intValue).sum() + nextInt <= 5);
assertThat(aggregated)
  .containsExactly(asList(1, 1, 2), asList(3), asList(4), asList(4), asList(5));

4. ストリーミング可能

Streamのインスタンスは再利用できません。 このため、 Streamableは、Stream:と同じメソッドをラップして公開することにより、再利用可能なストリームを提供します。

Streamable<String> s = Streamable.of("a", "b", "c", "d");
List<String> collected1 = s.collect(Collectors.toList());
List<String> collected2 = s.collect(Collectors.toList());
assertThat(collected1).hasSize(4);
assertThat(collected2).hasSize(4);

5. CollectorUtils

CollectorUtils は、いくつかの便利なコレクターメソッドを追加することにより、標準のCollectorsを補完します。

5.1.  maxBy()および minBy()

maxBy() は、提供されたプロジェクションロジックを使用してストリーム内の最大値を検索します。

Stream<String> clubs = Stream.of("Juventus", "Barcelona", "PSG");
Optional<String> longestName = clubs.collect(CollectorUtils.maxBy(String::length));
assertThat(longestName).contains("Barcelona");

対照的に、 minBy() は、提供された射影ロジックを使用して最小値を検出します。

5.2.  unique()

unique()コレクターは非常に単純なことを行います。指定されたストリームに1つの要素がある場合にのみ値を返します:

Stream<Integer> singleElement = Stream.of(1);
Optional<Integer> unique = singleElement.collect(CollectorUtils.unique());
assertThat(unique).contains(1);

それ以外の場合、 unique()は例外をスローします。

Stream multipleElement = Stream.of(1, 2, 3);
assertThatExceptionOfType(NonUniqueValueException.class).isThrownBy(() -> {
    multipleElement.collect(CollectorUtils.unique());
});

6. 結論

この記事では、ProtonpackライブラリがJavaStreamAPIを拡張して使いやすくする方法を学びました。 これは、私たちが一般的に使用する可能性があるが、標準APIにはない便利なメソッドを追加します。

Java 9以降、Protonpackが提供する機能の一部は標準ストリームAPIで利用できるようになります。

いつものように、コードはGithubにあります。