絵で理解するリスト処理 - java8 stream / javaslang -


「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);ってことで、ABにはStringOptional<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 -> RString -> 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) -> T0, (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 - のおまけ

スッキリ。