読者です 読者をやめる 読者になる 読者になる

エンタープライズギークス (Enterprise Geeks)

企業システムの企画・開発に携わる技術者集団のブログです。開発言語やフレームワークなどアプリケーション開発に関する各種情報を発信しています。ウルシステムズのエンジニア有志が運営しています。

Java8 Stream APIの基本(1) - 代表的な中間操作

Java8で新たに提供されたStream APIの基本をまとめておく。
(今回は並列処理機能については割愛する。)

概要

Stream API はJava8から追加されたAPIであり、高機能な繰り返し処理を記述できる。同じくJava8から追加されたラムダ式の使用を前提としている。

Stream API は次の3つに分類される。

  1. 生成処理 (Source)
    既存データからStreamを生成する。代表格はListSetstreamメソッドだが、Streamインターフェースにも多数の生成用のstaticメソッドがある。
  2. 中間操作 (Intermediate Operation)
    Streamのデータ加工を行う。代表格はfilter, map, flatMapで、他にもlimit, skipなどがある。
  3. 終端操作 (Terminal Operation)
    Streamの全データを一括で別のデータに変換する。代表格はcollectメソッドで、他にもforEach, reduce, count, findAnyなどがある。

中間操作を実行しても即座にStreamのデータが変更されたり評価されたりするわけではなく、終端操作実行時に1度にまとめて実行される(=厳密には異なるが、関数型言語の遅延評価のような性質を持つ)。
そのため中間操作を何度行っても、実際にループが行われるのは基本的に1回だけである。

終端操作は1つのStreamに1回だけしか実行できない。
終端操作を行った後のStreamを再利用すると例外が発生する。(countを取るだけでStreamは使用できなくなるので、Listのような使い方を想定すると混乱するかもしれない)。

従来のfor文に書いていたような判定や加工処理はStreamでは中間操作に置き換わる。中間操作はメソッドチェーンで操作をつなげていくことが可能であり、処理順序の入れ替えが容易である。

以降、主要な中間操作のfilter, map, flatMapを解説する。
この3つを使いこなせれば従来のfor文で行う処理の大半は記述できるはずである。

filter

filterPredicate<T>Streamの要素Tを引数に取り、booleanを返す関数)を引数に取るメソッドである。StreamからPredicatetrueを返す要素のみを抽出する。
(従来のコレクション走査なら、for文中のif文に相当する。)

サンプル

    // 偶数の抽出
    List<Integer> list = Arrays.asList(1,2,3,4,5);
    list.stream() // Streamの生成
        .filter(n -> n % 2 == 0)  // n % 2 == 0となる要素のみ抽出
        .forEach(System.out::println); // 各要素を出力。 結果は2,4

map

mapFunction<T,R>Streamの要素Tを引数に取り、R型の戻り値を返す関数。TRは同じ型でも良い)を引数に取り、Streamの要素を1対1で変換するメソッドである。要素の計算や型変換などに利用できる。
(従来のコレクション走査なら、for文の内部に書くロジックに該当する。)

サンプル

    // 要素の2乗
    List<Integer> list=Arrays.asList(1,2,3,4,5);
    list.stream() // Streamの生成
      .map(n -> n * n) // 要素の2乗(IntegerからIntegerへの変換)
      .map(n -> "answer = " + n) // (IntegerからStringへの変換)
      .forEach(System.out::println); // answer = xx の形式で出力

filtermapを組み合わせることで、抽出と変換をまとめて行える。

    // 奇数要素の2倍
    List<Integer> list=Arrays.asList(1,2,3,4,5);
    list.stream()
      .filter(n -> n % 2 == 1) // 奇数の要素のみを抽出し、
      .map(n -> n * 2) // 2倍にすると
      .forEach(System.out::println); // (2,6,10)が出力される

    // 2倍にしてから奇数要素の抽出(filterとmapを入れ替える)
    list.stream()
      .map(n -> n * 2) // 最初に2倍にすると
      .filter(n -> n % 2 == 1) // 奇数の要素は抽出されず
      .forEach(System.out::println); // 出力なし

上記の例の2つの処理はmapfilterの順序を入れ替えただけである。これと同様の処理をfor文で書こうとすると、加工処理をif文の中に書くか、外に書くかという違いが出る。このようにStreamを使えば処理の順序を簡単に変更できる。

flatMap

flatMapmapと似ているが、引数にFunction<T, Stream<R>> という1つの要素TからStream<R>型を生成する関数を取る。
また、flatMap自体の戻り値もStream<R>で、元のStream<T>の各要素から生成したStream<R>を1つのStream<R>としてまとめる。
(データの1対多への増幅・あるいは多重ループみたいなもの)。

サンプル

整数を取り、整数の数だけ同じ数字のStreamを生成する関数を用意する。

    Function<Integer, List<Integer>> repeat
        = n -> Stream.generate(() -> n).limit(n);
    repeat.apply(1).forEach(System.out::print); // 1
    repeat.apply(2).forEach(System.out::print); // 22
    repeat.apply(3).forEach(System.out::print); // 333

これを整数リストのStreamに適用してみる。

    List<Integer> list = Arrays.asList(0,1,2,4);
    list.stream()
      .flatMap(repeat) // 1224444 へ増幅。(0は増幅されない)
      .flatMap(repeat) // 増幅された各要素にさらにrepeatを適用して増幅
      .forEach(System.out::print); //122224444444444444444

このように、flatMapを実行するたびにデータが増幅・展開される。
このflatMapを使うことで、従来の多重ループに該当する処理が記述できる。

    IntStream.range(0, 4).boxed() // boxedはIntStreamからStreamへの変換。
        .flatMap(i -> IntStream.range(0, 4).boxed() // 最初に生成された各数値ごとにさらに増幅。
            .map(j -> i + ":" + j)) // 生成された数字を2つとも使うためには、このようにmapをネストさせる。
        .forEach(System.out::println); // 0:0,0:1,0:2,0:3,1:0,,,と16行生成。

これは以下のコードと同義である。

    for(int i = 0; i < 4; i++) {
        for (int j = 0; j < 4; j++) {
            System.out.println(i + ":" + j);
        }
    }

上記のように、flatMapを上手く使うとmapではできない様々なデータ加工を行うことができる。
(mapはあくまで1対1のデータ変換なので、Listを生成する関数をmapに渡すと、Listを要素とするStreamに変換されるだけになる。)

まとめ

filter, map, flatMapを組み合わせることで様々な処理をStreamで行える。
ただし以下のような処理を行うためには工夫が必要となるので、その話はまた別の機会に。

  • 2つのリストを比較する処理
  • 処理対象の要素と前後の要素を比較する処理
  • リストのインデックスを参照する処理(偶数番目の要素だけを処理したいなど)

[前多 賢太郎]