Java8入門:Stream API

Java Logo

Java8で追加されたStream API。いよいよ本丸である。

Stream APIの特徴

Java8入門:Lambda式で書いたように、並列処理を簡単に行えるようにする目的がある。単なる繰り返し処理を行う方法ではないのだ。

StreamAPIを利用することで以下のメリットがある。

  • 繰り返し処理を分かりやすく記述できる
  • 並列処理を簡単に導入できる

Streamは生成処理・中間処理・終端操作の3段階がある。生成処理でStreamを生成し、中間処理はStreamのデータの絞り込みや変換を行い、終端操作で結果を取り出す。

また、Streamには以下の特徴がある。

  • 終端操作を行ったタイミングで、遡って中間処理が実行される
  • 終端処理を行ったストリームの再利用はできない

使い方

Stream (Java Platform SE 8 )

生成処理・中間処理・終端操作で使えるメソッドは、Java Streamメモ(Hishidama’s Java8 Stream Memo)に良い感じにまとまっている。

GroupingByメソッド

groupbyメソッドが使えそうである。リストの中にある要素を、特定の条件で集約することができる。

Sample/StreamGrouping.java

Map<String, List<Person>> personGroupAByCity = groupA.stream()
    .collect(Collectors.groupingBy(p -> p.city));

集約条件を複数にすることもできる。以下のコードでは、cityで集約した後、年齢で集約している。

Map<Object, Map<Object, List<Person>>> personGroupBByCity = 
    groupB.stream()
      .collect(Collectors.groupingBy(p -> p.city, 
        Collectors.groupingBy(p -> p.age)));

データを処理した後、特定の条件で集約して、オブジェクトに変換するといった流れになるだろう。ただ、、Mapのメソッドが弱すぎて、Mapを処理するのがめんどくさい・・。2つのMapを結合したりできないのだろうか。。

パラレルストリーム

並列処理にするには、parallelメソッドを挟むだけ。これでマルチコアを生かした処理が書けるのだから、本当にありがい機能である。

// 並列処理しない
IntStream.range(0, 100)
  .forEach(System.out::println);

// 並列処理する
IntStream.range(0, 100)
  .parallel()
  .forEach(System.out::println);

ただし、処理が複数に分かれるため、結果は順番に並ばない。順番を意識するような処理には使えない。

その他、いろいろな注意点があり、以下のことを気をつけて実装を行う。

  • ストリームのサイズによっては、オーバーヘッドの方が大きくなる
  • ストリームの処理内では例外を発生させないようにする
  • 外部変数へアクセスしないようにする

2つのStreamを結合する

Java8のStreamを使いこなす - きしだのはてなの「zipでふたつのStreamを統合する」を見ると、Java8にzipメソッドはないようだ。

例えば、ファイルを読み込んで特定の文字列がある行を抽出し、行番号とその行を出力したいときに、zipメソッドは使える。行番号Streamと行データStreamを結合できれば、行番号とデータが一緒になるので、順番に処理しなくても良い。つまり、並列で処理できるようになる。

2つのデータを結合すると、2つのデータを一緒に扱いたくなる。ScalaでいうとToupleが欲しい、と思っていたら、以下の記事を見つけた。

Commons LangのPairクラスとzipメソッドを使って、特定の文字列を含む行を取り出す。こんな感じのソースコードになる。

Sample/PairSample.java

Path path = Paths.get("RFC_HTTP.txt");
try (Stream<String> lines = Files.lines(path)) {
  Stream<Integer> lineNums = IntStream.iterate(1, n -> n+1).boxed();
  zip(lineNums, lines)
    .parallel()
    .filter(p -> p.getRight().contains("HTTP"))
    .forEach(p -> System.out.println(p.getLeft() + "行目:" + p.getRight()));
}

どう使うか?

繰り返し処理を使っているところの置き換えがメインになるだろう。並列化できるところはparalle()を使っていく。より分かりやすく記述することができるようになった。

参考

Java7/8を勉強するのにおすすめの本。
今回も参考にさせていただいた。

現場で使える[最新]Java SE 7/8 速攻入門
櫻庭 祐一
技術評論社
売り上げランキング: 467,147
投稿日 2017年11月25日