「java8 で stream やってみたけど慣れねー」とか「ちょっと難しいことしようとするとできねー」ってのを良く聞くのでまとめてみるよ。
型さえ把握すれば8割方わかった様なもんなので、それを絵にしてみるよ。
ちなみに、記事は長いけど最初の[本編 -って見出しの箇所まで読めば十分だ!
やっぱ冷静に考えると長すぎるので、分割した。
[本編 - java8 stream]: filter, map, reduce
基本の3手を押さえよう。
reduceには馴染みがないかもしれないけど、Google が提唱したMapReduceモデルってのでmapとは一緒に語られるよ。-> wiki
以下の例題を使って見て行くよ。
/*
* 以下の様なログがある
* ラベル もしくは ラベル:millisec の形式で1行ずつ出力される
*
* millisec の総和を求めよ
*/
List<String> lines = Arrays.asList(
"application-boot", "access-dispatch:10", "dispatched", "authentication:30", "database-access:45", "response-creation:15", "access-logging"
);
大事なのはとにかく型なので、Generics にビビらずにそこだけはしっかり確認していくよ。
それから、こんな絵が大量に出てくるよ。
A -> B fって書いてあるのは java のコードにするとB f(a);ってことで、AやBにはStringやOptional<Integer>などの具体型が入ります。つまり Generics のことです。
filter
filterの定義はこう。
Stream<T> filter(Predicate<? super T> predicate);
Predicate<T>は「引数がTで戻り値がbool」ってこと、? super Tとかは今は忘れておっけー。
Predicate<T>の型表記をT を bool にするって捉えて、(T -> bool)って書いてみよう。
ついでに Generics も簡略化してしまおう。
Stream<T> filter((T -> bool) f);
filterを使ってlinesを:のある行だけにするのは、こう書ける。
lines.stream()
.filter(line -> line.contains(":")) // ["access-dispatch:10", "authentication:30", "database-access:45", "response-creation:15"]
定義をT -> boolと書いてみると、実装のline -> line.contains(":")が同じ様に見える。
(このお題では、Tは具体的にはStringってこと)
絵に描くとこんな感じだろうか。
filterは 型 は変わらなくて、数 が変わる変換ってことだ。
map
mapの定義はこう。
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
こっちも同じ様に簡略化すると、こうだ。
Stream<R> map((T -> R) f);
mapを使ってラベル:millisecの行をIntegerにするのは、filterの続きにこう書ける。
lines.stream()
.filter(line -> line.contains(":")) // ["access-dispatch:10", "authentication:30", "database-access:45", "response-creation:15"]
.map(line -> Integer.valueOf(line.split(":")[1])) // [10, 30, 45, 15]
ここも定義のT -> Rと実装が同じ形をしている。
(このお題では、Rは具体的にはIntegerってことだから、T -> RがString -> Integerに見えていると良い感じ)
絵に描くとこんな感じだろうか。
mapは 数 は変わらなくて、 型 が変わる変換ってことだ。
reduce
reduceの定義は java8 stream には3つあるけど、今から使うのはこうだ。
T reduce(T identity, BinaryOperator<T> accumulator);
BinaryOperator<T>は少し見慣れないけど、「引数がTを2つで戻り値もT」ってことだ。
これもビビらずに簡略化してみよう。
T reduce(T t, ((T, T) -> T) f);
reduceは一般に「畳み込み」と言われていて、リストの先頭から順に結果を蓄積していく処理だ。
reduceを使ってIntegerの行をIntegerの総和にするのは、mapの続きにこう書ける。
lines.stream()
.filter(line -> line.contains(":")) // ["access-dispatch:10", "authentication:30", "database-access:45", "response-creation:15"]
.map(line -> Integer.valueOf(line.split(":")[1])) // [10, 30, 45, 15]
.reduce(0, (acc, n) -> acc + n) // 100
定義に少し括弧が多いけど、T, (T, T) -> Tと0, (acc, n) -> ...が同じ形をしているから少しはわかりやすいはず。
絵に描くとこんな感じだろうか。
reduceは リストの要素 が 単一の結果 になる処理だ。
まとめ
これでクリアー!
* millisec の総和を求めよ
は
// ( 再掲 )
lines.stream()
.filter(line -> line.contains(":")) // ["access-dispatch:10", "authentication:30", "database-access:45", "response-creation:15"]
.map(line -> Integer.valueOf(line.split(":")[1])) // [10, 30, 45, 15]
.reduce(0, (acc, n) -> acc + n) // 100
で解けた。
一応全体の絵を載せておく。
おわり
ここから先の全てのおまけは以下の記事に分離した。
絵で理解するリスト処理 - java8 stream / javaslang - のおまけ
スッキリ。