Stream を使い回そうとする
前述の通り、ターミナル操作を呼んだ Stream は閉じます。Stream<Integer> s = ...; s.count(); s.forEach(...); のように 2 回呼ぶと例外で落ちます。中間状態を変数で取っておく書き方はやめて、メソッドチェーンで一気に書くのが鉄則です
レッスンの本文で注意している点を、つまずきの側から45件ぶん並べ直しました。書いた覚えのある形が見つかったら、 そのままレッスンに戻って前後を読めます。
前述の通り、ターミナル操作を呼んだ Stream は閉じます。Stream<Integer> s = ...; s.count(); s.forEach(...); のように 2 回呼ぶと例外で落ちます。中間状態を変数で取っておく書き方はやめて、メソッドチェーンで一気に書くのが鉄則です
Collection と Stream を混同するlist.stream() は新しい Stream を返すだけで、元の list は変わりません。list.stream().filter(...) の結果をどこにも代入せずに、「list から偶数だけ消えたはず」と思い込むのはよくあるミスです。Stream は元データを変更しません
Arrays.stream(arr).filter(n -> n > 0); と書いて「実行された」と思い込んでも、ターミナル操作がないので 何も起きません。これは 遅延評価 のせいで、filter を呼んだだけでは実際の処理が走らないからです。count() collect() forEach() などを最後に必ず付けましょう
filter の中で副作用を書く.filter(n -> { System.out.println(n); return n > 0; }) のようにログ出力やフィールド更新を filter の中に書くのは避けるべきです。filter は純粋に判定だけをするのが約束で、副作用を入れるとデバッグが難しくなり、並列ストリームでは予期しない動作になります。ログを入れたいときは専用の peek を使うか、デバッグ用に一時的に挟むだけにしましょう
filter を分けて書きすぎる.filter(n -> n > 0).filter(n -> n < 100).filter(n -> n % 2 == 0) のように filter をひたすら連結する書き方はパフォーマンスは大きく変わりませんが、可読性は条件によって落ちることがあります。論理 AND でつながる条件は n -> n > 0 && n < 100 && n % 2 == 0 のように 1 つにまとめるか、逆に意味の単位で分割するかを意識すると読みやすくなります
null 要素が混ざる可能性を忘れるStream<String> のように参照型を扱う場合、要素に null が混ざることがあります。.filter(s -> s.length() > 0) と書くと、null 要素で NullPointerException が出てしまいます。先に .filter(s -> s != null) で null を弾いてから本来の条件を書く、または .filter(Objects::nonNull) を使うのが定石です
map の中で副作用を書くmap(x -> { System.out.println(x); return x; }) のように、変換のついでに println やフィールド書き換えをするのは Stream の作法に反します。表示やログ出力は peek か終端操作の forEach で行うのがルールです。副作用入りの map は並列ストリームで予期しない順序で動いて、バグの温床になります
IntStream の map は int -> int しか受け付けません。IntStream.of(1, 2, 3).map(x -> "num=" + x) のように String を返そうとすると怒られます。型を変えたいときは mapToObj を使うのが正解です
mapmap(x -> something(x)) の中で null を返すと、後続の処理で NullPointerException が出やすくなります。null を返す可能性があるなら、Optional でラップするか filter(Objects::nonNull) で前処理するのが安全です
identity を省略して Optional を扱い忘れるreduce(BinaryOperator) のように引数 1 つ版もありますが、こちらは空 Stream の可能性があるため戻り値が Optional<T> になります。int product = stream.reduce((a, b) -> a * b); と書いてもコンパイルすら通らず、Optional<Integer> を int に代入できないと怒られます。今回のように空配列でも 1 を返したい場合は、identity を渡す 2 引数版を使うのが圧倒的に楽です
accumulator に副作用を入れるreduce の関数の中で System.out.println を呼んだり、外側のフィールドを書き換えたりするのは禁じ手です。並列化したときに表示順がぐちゃぐちゃになったり、ConcurrentModificationException が出たりして、デバッグ困難なバグの温床になります。accumulator は「a と b を受け取って新しい値を返すだけの純粋関数」に保つ、というのが鉄則です
reduce(0, (a, b) -> a - b) のように引き算で書くと、順次実行では 0 - 1 - 2 - 3 = -6 になりますが、並列で (0 - 1) と (2 - 3) を計算して合成すると結果が変わってしまいます。減算や除算は結合的ではないので、reduce の accumulator には使わないのが原則です。どうしても引き算したいときは、map(x -> -x).reduce(0, Integer::sum) のように加算に変換するか、for ループで書くかを検討します
toMap のキー重複toMap(keyMapper, valueMapper) でキーが重複すると IllegalStateException: Duplicate key が飛びます。マージ方法を決めるには 3 引数版の toMap(k, v, (a, b) -> a) で「重複したら先勝ち」のように指定するか、そもそも groupingBy で List にまとめるのが定石です
joining の delimiter 指定忘れCollectors.joining() を引数なしで呼ぶと区切り文字なしで全部くっつきます。CSV のつもりで joining() と書いてしまうと "246" のようになるので、joining(",") を渡すのを忘れないようにしましょう
toList() の戻り値を変更しようとするJava 16 から増えた Stream.toList() (引数なし) は immutable な List を返します。後で add したいなら Collectors.toList() を使うか、new ArrayList<>(immutableList) で詰め替えてください
sorted(null) のように Comparator に null を渡すJava 8 の仕様では、sorted(Comparator) に null を渡すと NullPointerException が飛びます。sorted() (引数なし) は自然順を意味するので、「自然順がほしい」なら 引数を渡さない ことが正解です。null をデフォルト扱いにしてはいけません
Comparable) を実装していない型で sorted() を呼ぶ自作クラス (たとえば class Point {}) のリストを sorted() (引数なし) で並べ替えようとすると、実行時に ClassCastException: ... cannot be cast to class java.lang.Comparable が出ます。自作クラスを自然順で並べたいなら implements Comparable<Point> を書いて compareTo を実装するか、sorted(Comparator.comparing(...)) を渡してください
Comparator に副作用を持ち込むsorted((a, b) -> { counter++; return a - b; }) のように Comparator の中で外部変数を書き換えると、ソート結果が並列実行や JIT の最適化で予測不能になります。Comparator は「同じ入力に同じ出力を返す純粋関数」として書く、というのが鉄則です
equals / hashCode を実装し忘れる前述のとおり、自作クラスを distinct() に通しても参照比較になり、重複排除されません。record を使うか、IDE の自動生成で equals と hashCode を必ずペアで書きましょう
Stream は一度終端操作を呼ぶと閉じられ、IllegalStateException が出ます。Stream<String> s = list.stream(); を 2 回使い回すのではなく、必要に応じて list.stream() を再度呼び直すのが正解です
distinct() は元の Stream の出現順を維持します。{3, 1, 2, 1, 3} を流すと {3, 1, 2} の順で残ります。並べ替えたいときは別途 sorted() を繋いでください。逆に parallelStream() では順序保証が崩れる場合があるので、順序が重要なら stream() を使うか forEachOrdered を選びます
skip → limit と limit → skip の順序を取り違えるstream.skip(2).limit(3) は「先頭 2 件を捨ててから 3 件取る」(3, 4, 5 番目を取得)、stream.limit(3).skip(2) は「先頭 3 件に絞ってから 2 件捨てる」(結果は 1 件) と挙動が変わります。ページネーションは必ず skip → limit の順番で書きます
limit を付け忘れるStream.iterate や Stream.generate から作った無限 Stream を forEach や count に直接渡すと、プログラムが止まらなくなります。途中で必ず limit か takeWhile などで打ち切りましょう
limit(-1) skip(-1) のように負の引数を渡すlimit も skip も負の値はサポートしておらず、実行時に IllegalArgumentException を投げます。page - 1 を計算するときに、page が 0 や負の値になっていないかを必ず検証してください
parallel() を付ければ順序が保たれていると思い込むforEach の出力順、reduce の集合体の組み立て順は 実行のたびに変わる と思って書く必要があります。順序が必要なら forEachOrdered か Collectors.toList() を選びましょう
非結合的 な reducer を渡してしまう引き算や、文字列を結合順序つきで作る処理は、並列だと結果が変わります。reduce("", String::concat) を並列で回すと、繋がる順番が毎回違うので、最終の文字列がランダムに並び替わったように見えます
parallel() を付けて逆に遅くなる配列の要素数が数百〜数千程度では、並列化のオーバーヘッドのほうが大きく、for のほうが速いことが普通です。並列はデータが多くて、要素 1 個の計算が重いとき だけが効きます
n -> n * 2 は return を書かない、n -> { return n * 2; } は書く、というルールです。n -> { n * 2; } のように return を抜くとコンパイルエラーになります。{} を付けたら必ず return を書く、と覚えてください
ラムダの中で参照する外側のローカル変数は final または effectively final (代入が 1 度きり) でなければなりません。int x = 10; Function<Integer,Integer> f = n -> n + x; まではよくても、その後 x = 20; と書き換えるとエラーになります。インスタンスフィールドなら自由に書き換えできますが、ローカル変数は固定です
return を書ける場所と書けない場所を混同する式形式は値そのものを返すので return を書きません。ブロック形式は普通のメソッドと同じく return で値を返します。式形式の右側にうっかり return n * 2 と書くとコンパイルエラーになるので注意してください
BiFunction<Integer, Integer, Integer> に対して Integer::parseInt を渡すとコンパイルエラーです。parseInt は 1 引数なので、2 引数のインターフェースには使えません。ラムダで書いたときに渡している引数の数と、メソッドの引数の数を必ず一致させることを意識しましょう
Function<String, Integer> のところに Function<String, Long> を返すメソッド参照を渡そうとすると incompatible types のエラーになります。Long::parseLong と Integer::parseInt を混同しがちなので、戻り値の型まで揃っているか確認してください
ClassName.new と書く正しくは ClassName::new です。.new は内部クラスのインスタンス生成用の別文法なので、ここで使うと意味が変わってしまいます。::new という記号の並びをそのまま覚えるのが安全です
Function と Consumer を混同するFunction<T, R> は「値を返す」関数、Consumer<T> は「値を受け取って戻り値なし (void) 」の関数です。System.out.println を渡したいだけなら Consumer 、結果を返してほしいなら Function 、と用途で選び分けます。間違って Function<Integer, Void> のような無理な型を作る前に、Consumer の存在を思い出してください
var f = x -> x * 2; のように var でいきなり受けようとすると、コンパイラが「ラムダ式単体では型を決められない」と言ってエラーになります。必ず Function<Integer, Integer> f = x -> x * 2; のように 左辺で型を宣言 するか、メソッド引数として渡してその場で文脈から型を決めるかのどちらかにします
null 引数を素通しさせるFunction<String, Integer> parser = Integer::parseInt; に null を渡すと NullPointerException が出ます。Function 自体は null を弾く仕組みを持たないので、null の可能性があるときは事前に null チェックするか、Optional<T> を組み合わせるかで身を守る必要があります
test の戻り値型を勘違いするPredicate<Integer> を作るときに、つい n -> n * 2 のような「整数を返すラムダ」を書いてしまうケースです。Predicate.test は必ず boolean を返す約束なので、n -> n * 2 は Predicate<Integer> としては成立せず、incompatible types というコンパイルエラーになります。整数を別の整数に変換したいときは Predicate ではなく Function<Integer, Integer> を使うのが正解です
null 判定を手書きしすぎる参照型を扱う Predicate<String> などでは、要素に null が混ざることがあります。素直に s -> s != null && s.length() > 0 と書いてもよいのですが、null チェック専用のメソッド参照 Objects::nonNull が java.util.Objects に用意されています。Predicate<String> notNull = Objects::nonNull; のように書くと、意図が一発で伝わるのでおすすめです
negate の順序を間違えるisPositive.and(isEven).negate() と isPositive.and(isEven.negate()) は、見た目は似ていますが意味がまるで違います。前者は「正かつ偶数」の否定 (= 負または奇数)、後者は「正かつ奇数」です。negate がどの Predicate にかかっているかを意識して、必要なら (...) で明示するか、いったん変数に入れて段階的に組み立てるのが安全です
Consumer の中で return を書いてしまうConsumer<String> c = s -> { return s.length(); }; のように戻り値を書くとコンパイルエラーになります。accept の戻り値は void なので、ラムダの中も値を返してはいけません。値を返したいのは Function<T, R> の役割です
Supplier.get() を毎回呼ぶのに副作用付きのラムダを書いてしまうSupplier<Integer> next = () -> counter++; のように、内側でフィールドを書き換える Supplier は危険です。同じ Supplier を get() するたびに別の値が返ってくるので、ライブラリ側がいつ呼ぶかわからない (orElseGet Stream.generate 等) 文脈で予測不能なバグになります。Supplier は基本的に副作用なしで書きましょう
hello.get のようにかっこを忘れて参照だけ書くhello は Supplier<String> ですが、String s = hello; と書くと型が違うとエラーになります。値を取り出すには必ず hello.get() のようにかっこ付きで get() を呼びます。Consumer も同様で、呼び出すときは printer.accept("x") の形が必要です
BiFunction<Integer, Integer> のように 2 個しか書かないと「型パラメータが足りない」エラーになります。BiFunction は引数 2 個 + 戻り値 1 個の 合計 3 個 が必須です。逆に Function<T, R> は 2 個、Predicate<T> は 1 個、と数が違うので、ペアで覚えるのがコツです
BiFunction で書こうとする標準ライブラリには BiFunction までしかありません。TriFunction を import しようとして cannot find symbol で立ち止まる人が多いですが、これは 自作するか、引数をまとめる のが正解です。java.util.function.TriFunction というクラスは存在しません
Function と混同するFunction<Integer, Integer> f = (x, y) -> x + y; は文法エラーです。Function は引数 1 個なので、ラムダも (x) -> ... か x -> ... の形である必要があります。2 引数を受け取りたいなら型を BiFunction に変えるか、ラムダの引数を 1 つにします