JavaのストリームAPI

プログラミング

プログラムはできるだけシンプルに分かりやすく記述することで、バグや認識違いを少なくすることができます。

JavaのストリームAPIは、プログラムをシンプルに記述することができる手法の1つです。

本記事では、JavaのストリームAPIについて解説します。

記事内に記載しているプログラムは、Java11を使って動作確認をしています。

JavaのストリームAPI

JavaのストリームAPIについて、以下の内容を解説します。

  • ストリームAPIとは
  • ストリームAPIの操作
  • 中間操作
  • 終端操作
  • 直列ストリームと並列ストリーム

ストリームAPIとは

ストリームAPIとは、Java8より追加されたイテレーション操作を拡張するAPIです。

CPUがマルチコア化していることに伴い、効率よく処理するためには、プログラム側もマルチコア対応する必要がありました。
Java7以前において大きな処理単位での並列処理は出来たものの、ちょっとしたループ処理においてもマルチコアによる処理効率をあげたいとの提案から導入されたと言われます。

そのため、全く新しい方法というわけではなく、既存のコレクションに対して内部的にイテレーション操作を行うためのAPIとして追加されました。

大抵のループ処理は、ストリームAPIを使う方法に置き換えることができ、簡潔に記述かつ効率よく処理することができます。

また、ストリームAPIの構文はラムダ式を用いて記述することが前提となっています。
ラムダ式が理解できていないという方は、先に以下の記事をご覧いただくことをオススメします。

Javaのラムダ式入門
Java8より追加されたラムダ式ですが、理解せずに何となく使っていたり、うまく使いこなせていなかったりしないでしょうか。 本記事では、Javaのラムダ式の入門ということで、一緒に説明されることの多いStreaming APIとは切り離して、ラムダ式だけを解説します。

ストリームAPIの操作

ストリームAPIによる操作と、これまでの違いを比較するために、従来の方法で記述したプログラムが以下となります。

List list = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
for (int i : list) {
    if (i % 2 == 0) {
        System.out.println(i);
    }
}

1~10の要素を持つリストをループで回して、偶数のみを表示しています。
このプログラムをストリームAPIを使って記述すると、以下のようになります。

List list = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
list.stream()
    .filter(i -> i % 2 == 0)
    .forEach(System.out::println);

長いので3行にしていますが、1つの文で記述することができ、簡潔で直感的に分かりやすい記述になっています。
上記のプログラムでは、以下の3つの役割に分かれています。

  • ストリームの取得:ストリームの種類を指定して操作を開始する
  • 中間操作:ストリームの各要素に対して処理を行う
  • 終端操作:中間操作した要素全体に対して結果の処理を行う

中間操作

中間操作はストリームの各要素に対して、フィルタリングや変換等の処理を行って、次の操作へ渡します。
中間操作をいくつもつなげることは可能ですが、中間操作で終了することは出来ません。

主な中間操作の種類について、紹介していきます。

フィルタ操作

filterは、各要素に対して条件に一致する要素を次のストリームに渡します。

List list = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
List r1 = list.stream()
    .filter(i -> i % 2 == 0)
    .collect(Collectors.toList());
System.out.println(r1); // -> [2, 4, 6, 8, 10]

上記のプログラムでは、偶数のみにフィルタリングしています。

変換操作

mapは、各要素に対して任意の変換を行って、次のストリームに渡します。

List list = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
List r2 = list.stream()
    .map(i -> String.format("#%d", i))
    .collect(Collectors.toList());
System.out.println(r2); // -> [#1, #2, #3, #4, #5, #6, #7, #8, #9, #10]

上記のプログラムでは、数値を文字列に変換しています。

ソート

sortedは、ストリームの各要素に対してソートを行って、次のストリームに渡します。

List list = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
List r3 = list.stream()
    .sorted(Comparator.reverseOrder())
    .collect(Collectors.toList());
System.out.println(r3); // -> [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]

上記のプログラムでは、数値を降順に並び替えています。

要素数制限

limitは、先頭から指定した数までの要素を次のストリームに渡します。

List list = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
List r4 = list.stream()
    .limit(5)
    .collect(Collectors.toList());
System.out.println(r4); // -> [1, 2, 3, 4, 5]

上記のプログラムでは、先頭から5つ目までの要素に制限しています。

skipは、指定した数まで飛ばして、それ以降の要素を次のストリームに渡します。

List list = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
List r5 = list.stream()
    .skip(5)
    .collect(Collectors.toList());
System.out.println(r5); // -> [6, 7, 8, 9, 10]

上記のプログラムでは、先頭から5つの要素を飛ばして、6つ目以降の要素に制限しています。

重複排除

distinctは、要素の重複を排除して次のストリームに渡します。

List list2 = List.of(1, 2, 1, 4, 2, 3, 3, 4, 5, 1);
List r6 = list2.stream()
    .distinct()
    .collect(Collectors.toList());
System.out.println(r6); // -> [1, 2, 4, 3, 5]

上記のプログラムでは、同じ数値の要素を排除しています。

デバッグ用アクション実行

peekは、各要素に対して処理を行うことができますが、要素そのものは変更せずに次のストリームに渡します。
主な用途としては、デバッグ用のログ出力などに用いられます。

List list = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
List r7 = list.stream()
    .peek(System.out::println)
    .collect(Collectors.toList());
System.out.println(r7); // -> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

上記のプログラムでは、各要素の値を表示していますが、最終結果に影響を与えることはありません。

終端操作

終端操作では、中間操作での結果をまとめて最後の処理を行います。
終端操作は必ず必要であり、1回だけ指定することができます。

繰り返し処理

forEachは、ストリームの各要素に対して繰り返し処理を行います。

StringBuilder sb = new StringBuilder();
List list = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
list.stream()
    .map(i -> "#" + i)
    .forEach(s -> sb.append(s));
System.out.println(sb); // -> #1#2#3#4#5#6#7#8#9#10

上記のプログラムでは、中間操作で変換した文字列を、繰り返し処理で結合する処理を行っています。

一致判定

allMatchは、ストリームの各要素のすべてが指定した条件に一致しているかを判定します。

List list = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
boolean r8 = list.stream()
    .filter(i -> i % 2 == 0)
    .allMatch(i -> i == 2);
System.out.println(r8); // -> false

上記のプログラムでは、各要素がすべて2であるかどうかを判定しています。
中間操作で偶数にフィルタリングし、2以外も含まれているので、結果はfalseとなります。

anyMatchは、ストリームの各要素のいずれかが指定した条件に一致しているかを判定します。

List list = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
boolean r9 = list.stream()
    .filter(i -> i % 2 == 0)
    .anyMatch(i -> i == 2);
System.out.println(r9); // -> true

上記のプログラムでは、各要素がとれかが2であるか(2が含まれているか)どうかを判定しています。
中間操作で偶数にフィルタリングし、2が含まれているので、結果はtrueとなります。

noneMatchは、ストリームの各要素のどれかが指定した条件に一致していないかを判定します。
anyMatchの逆で、含まれていればfalse、含まれていなければtrueとなります。

List list = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
boolean r10 = list.stream()
    .filter(i -> i % 2 == 0)
    .noneMatch(i -> i == 2);
System.out.println(r10); // -> false

上記のプログラムでは、各要素がどれかが2でないかどうかを判定しています。
中間操作で偶数にフィルタリングし、2が含まれているので、結果はfalseとなります。

要素数の取得

countは、ストリームの各要素の数を返します。

List list = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
long r11 = list.stream()
    .filter(i -> i % 2 == 1)
    .count();
System.out.println(r11); // -> 5

上記のプログラムでは、偶数にフィルタリングして残った要素の数を取得しています。

集計

sumは、要素の数値の合計を取得します。

List list = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
int r12 = list.stream()
    .mapToInt(i -> i)
    .sum();
System.out.println(r12); // -> 55

上記のプログラムでは、リスト内の要素の合計を計算しています。

maxは、要素内の最も大きい数値を数値を返します。

List list = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
OptionalInt r13 = list.stream()
    .mapToInt(i -> i)
    .max();
System.out.println(r13.getAsInt()); // -> 10

minは、要素内の最も小さい数値を返します。

List list = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
OptionalInt r14 = list.stream()
    .mapToInt(i -> i)
    .min();
System.out.println(r14.getAsInt()); // -> 1

リストや配列として取得

collectは、ストリームの各要素をリストやセットなどのコレクションとして取得します。

List list = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
Set r15 = list.stream()
    .filter(i -> i % 2 == 0)
    .collect(Collectors.toSet());
System.out.println(r15); // -> [2, 4, 6, 8, 10]

上記のプログラムでは、結果をSetとして取得しています。

toArrayは、ストリームの各要素を配列として取得します。

List list = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
Integer[] r16 = list.stream()
    .filter(i -> i % 2 == 0)
    .toArray(Integer[]::new);
System.out.println(Arrays.toString(r16)); // -> [2, 4, 6, 8, 10]

直列ストリームと並列ストリーム

ストリームにはいくつか種類があり、主に直列ストリームと並列ストリームに別れます。

直列ストリームは、各要素を順番通りに順次中間操作、終端操作を行っていきます。
並列ストリームは、各要素を並列で中間操作、終端操作を実施していきます。並列で処理するため効率が良いですが、処理される要素の順序が不定となる点に注意が必要です。

上記で出てきたプログラムは、いずれも直列ストリームです。
並列ストリームを使ったプログラム例は以下となります。

List list = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
list.parallelStream()
    .mapToInt(i -> i)
    .forEach(System.out::println);

上記のプログラムは、リスト内の1~10の数値を表示しているだけですが、並列で操作されるため、表示される順番は不定で、例えば以下のようになります。

6
7
9
10
8
3
4
5
1
2

まとめ

JavaのストリームAPIについてまとめると、以下となります。

  • ストリームAPIとは、Java8より追加されたイテレーション操作を拡張するAPIで、主にループで行う処理を簡潔に記述することができる。
  • ストリーム処理は、ストリームの取得、中間操作、終端操作に別れ、用途に応じた様々な種類がある。
  • 並列ストリームを使うと処理効率は良いが、順序が不定になるため注意が必要。

一見すると難しいストリームAPIですが、ループのプログラムを書く際は、ストリームAPIが使えないか考えるところから始めてみてはいかがでしょうか。

 

今回は、JavaのストリームAPIについて解説しました。

以上、参考になれば幸いです。

コメント

タイトルとURLをコピーしました