Java8 Stream APIの基本(1) - 代表的な中間操作
Java8で新たに提供されたStream APIの基本をまとめておく。
(今回は並列処理機能については割愛する。)
概要
Stream API はJava8から追加されたAPIであり、高機能な繰り返し処理を記述できる。同じくJava8から追加されたラムダ式の使用を前提としている。
Stream API は次の3つに分類される。
- 生成処理 (Source)
既存データからStream
を生成する。代表格はList
やSet
のstream
メソッドだが、Stream
インターフェースにも多数の生成用のstaticメソッドがある。 - 中間操作 (Intermediate Operation)
Stream
のデータ加工を行う。代表格はfilter
,map
,flatMap
で、他にもlimit
,skip
などがある。 - 終端操作 (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
filter
は Predicate<T>
(Stream
の要素Tを引数に取り、boolean
を返す関数)を引数に取るメソッドである。Stream
からPredicate
がtrue
を返す要素のみを抽出する。
(従来のコレクション走査なら、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
map
は Function<T,R>
(Stream
の要素Tを引数に取り、R
型の戻り値を返す関数。T
とR
は同じ型でも良い)を引数に取り、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 の形式で出力
filter
とmap
を組み合わせることで、抽出と変換をまとめて行える。
// 奇数要素の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つの処理はmap
とfilter
の順序を入れ替えただけである。これと同様の処理をfor
文で書こうとすると、加工処理をif
文の中に書くか、外に書くかという違いが出る。このようにStream
を使えば処理の順序を簡単に変更できる。
flatMap
flatMap
はmap
と似ているが、引数に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つのリストを比較する処理
- 処理対象の要素と前後の要素を比較する処理
- リストのインデックスを参照する処理(偶数番目の要素だけを処理したいなど)
[前多 賢太郎]