Java SE 8のストリームAPIの正しい使い方──ラムダ式とともに導入された新APIで、並列処理の実装はどう変わるのか?

Oracle Java & Developers編集部
2014-11-28 11:00:00
  • このエントリーをはてなブックマークに追加

一連の処理をパイプラインとして実装する

 ここから、さらにリファクタリングを進めていきましょう。まずメソッドprocessMatchingPersonsでやりたいことを図示すると、下図のように整理できます。


※クリックすると拡大画像が見られます

 これをもっとシンプルに本質を図式化すると、次の図に示すようなパイプライン処理だと捉えることができるでしょう。フィルタリングの条件やマッピング、アクションなどはラムダ式を使って呼び出し側で指定します。パラメータとして値を渡すのではなく、振る舞いを渡しているところがポイントです。


※クリックすると拡大画像が見られます

 Java SE 8では、このようなパイプライン処理をそのままコード化できる新機能が追加されました。それがストリームAPIです。ストリームAPIを使えば、「16歳以上の人に電話をかける」というメソッドrobocallEligibleDriversの実装(リスト9)は、次のように書き直すことができます。

【リスト13:ストリームAPIを使って書き直したメソッドrobocallEligibleDrivers】

void robocallEligibleDrivers() { list.stream() .filter(p -> p.getAge() >= 16) .map(p -> p.getHomePhoneNumber()) .forEach(num -> robocall(num)); }

 ストリームAPIでは、フィルタリングやマッピングといった操作を連鎖的につないでパイプラインを構成することができます。Listオブジェクトからストリームを作るには、メソッドstreamを呼び出すだけです。すると、メソッドstreamはインタフェースjava.util.stream.Streamのインスタンスを返します。このインタフェースには、パイプラインを構成するためのさまざまな操作がメソッドとして定義されています。もし間に何らかの操作を付加したければ、メソッドの呼び出しを追加するだけで済みます。また、同じパイプラインでも、指定する振る舞いを変えるだけで実装を変更できるというメリットもあります。

ストリームAPIを使えば、パイプライン処理を簡単に実現できる

 ここで、ストリームとパイプラインの概念を簡単にまとめておきましょう。それぞれ、次のようになります。

【ストリーム】

  • ストレージ(またはコレクション)ではない
  • 順番は保証されない
  • イテレータのような順次処理ができる
  • 並列処理化をサポート

【パイプライン】

  • ソースを受け取る
  • 0個以上の中間操作で構成される
  • 1つの終端操作で終了する

 Java SE 8のストリームAPIでは、ストリームのソースとして次のようなものを使うことができます。

  • コレクション
  • 空(空のストリームを作成)
  • 引数指定
  • 配列、配列内の要素の一部
  • プリミティブ型の数値の範囲指定
  • ファイル

 次に示すのは、上記の各ソースからストリームを取得する方法です。

【リスト14:各ソースからのストリームの取得方法】

// コレクション List<String> list = Arrays.asList("to", "be", "or", "not", "to", "be"); list.stream() .forEach(s -> System.out.print(s + " ")); // 空のストリーム Stream.empty() .forEach(s -> System.out.print(s + " ")); // 引数指定 Stream.of("to", "be", "or", "not", "to", "be") .forEach(s -> System.out.print(s + " ")); // 配列、部分配列 int[] arr = new int[] { 0, 1, 2, 3, 4, 5, 6, 7 }; Arrays.stream(arr, 2, 6) .forEach(i -> System.out.print(i + " ")); // 数値の範囲 IntStream.range(10, 20) .forEach(i -> System.out.print(i + " ")); // ファイル try (BufferedReader reader = Files.newBufferedReader(Paths.get("/etc/hosts"))) { reader.lines() .forEach(line -> System.out.println(line)); }

 プリミティブ型のストリームは、IntStreamDoubleStreamといったインタフェースで定義されています。これらは無駄なボクシング(プリミティブな値からクラス・オブジェクトへの変換)を減らすために用意されたものです。

 ストリームに対する操作は、「中間操作」と「終端操作」に分けることができます。中間操作とはパイプラインの中で複数を連鎖的につなげて実行できる操作で、終端操作はパイプラインの最後の処理を行う操作です。終端操作はパイプラインに対して1つだけ実行でき、終端操作を行った後のストリームは再利用できません。

 中間操作のための主要なメソッドには、次のようなものがあります。

●map:フィルタリング。条件を満たす要素だけを残す

●filter:マッピング。各要素の値を他の値に変換する

●skip:指定した数の要素を読み飛ばす

●limit:要素数を制限する

●sorted:ソートする

●distinct:同一の要素を除外する

●peek:要素をそのまま次の操作に渡す

●flatMap:ストリームを返すマップを平坦化する

 これらのうち、主なメソッドの使用例を以下に示します。

【リスト15:中間操作のための主なメソッドの使用例】

List<String> list = Arrays.asList("to", "be", "or", "not", "to", "be"); // map list.stream() .map(s -> s.toUpperCase()) .forEach(s -> System.out.print(s + " ")); // 出力:TO BE OR NOT TO BE // filter list.stream() .filter(s -> s.contains("o")) .forEach(s -> System.out.print(s + " ")); // 出力:to or not to // skipとlimit list.stream() .skip(1) .limit(3) .forEach(s -> System.out.print(s + " ")); // 出力:be or not // sorted list.stream() .sorted() .forEach(s -> System.out.print(s + " ")); // 出力:be be not or to to // distinct list.stream() .distinct() .forEach(s -> System.out.print(s + " ")); // 出力:to be or not

 一方、終端操作のための主要なメソッドとしては、次のようなものがあります。

●forEach:各要素に対して、それぞれ処理を実行する

●collect:集約処理

●toArray:ストリームを配列に変換する

●count:要素数をカウントする

●findFirst:最初の要素を返す

●findAny:順序が保証されないストリームで、最初に見つけた要素を返す

●min:最小値を返す

●max:最大値を返す

●reduce:前回の値と今回の値で集約処理を行う

 以下に、上記の主なメソッドの使用例を示します。

【リスト16:終端操作のための主なメソッドの使用例】

List<String> list = Arrays.asList("to", "be", "or", "not", "to", "be"); // forEach list.stream() .forEach(s -> System.out.print(s + " ")); // 出力:to be or not to be // Collect List<String> output = list.stream() .map(s -> s.toUpperCase()) .collect(toList()); System.out.println(output); // 出力:[TO, BE, OR, NOT, TO, BE] // toArray String[] output = list.stream() .map(s -> s.toUpperCase()) .toArray(n -> new String[n]); System.out.println(output); // 出力:[Ljava.lang.String;@31befd9f // peekとcount long output = list.stream() .filter(s -> s.contains("o")) .peek(s -> System.out.print(s + " ")) .count(); System.out.println(); System.out.println(output); // 出力:to or not to // 4 // findFirst Optional<String> output = list.stream() .filter(s -> s.startsWith("n")) .findFirst(); System.out.println(output.get()); // 出力:not // findFirst(NoSuchElementExceptionの回避) Optional<String> output = list.stream() .filter(s -> s.startsWith("x")) .findFirst(); System.out.println(output.orElse("absent")); // 出力:absent

 メソッドfindFirstは要素のオブジェクトそのものではなく、要素が格納されたOptionalというオブジェクトを返す点に注意してください。もし条件にヒットする要素がなかった場合は、Nullではなく空のOptionalが返されます。このような実装になっているのは、戻り値に対するNullPointerExceptionを回避するためです。Optionalからはメソッドgetで要素を取り出せますが、getの代わりにメソッドorElseを使えば、要素が空の場合に返すデフォルトの値を指定することができます。

  • コメント(1件)
#1 勘違いかもしれませんが   2015-08-09 10:41:23
中間操作のための主要なメソッドの説明で
マップとフィルタの内容が入れ替わっているように思います