filter で絞り込む

生田 陸人
LuaGate エンジニア / 現役エンジニア
編集 LuaGate編集部

このレッスンで分かること

  • stream.filter(x -> 条件) で条件を満たす要素だけ通過させる
  • 中間操作 (遅延評価) ─ 終端操作までは実際にフィルタされない
  • Predicate<T> を引数に取る ─ x -> x > 0 形式のラムダ
  • 新しい Stream を返す、元のコレクションは変わらない

filter で絞り込む とは

Java の Stream API の中でも最頻出の filter を取り上げ、ラムダ式で条件を渡して要素を絞り込み、count で個数を求めるコードを書いてみよう。

なぜ filter から学ぶのか

Stream API には map filter reduce collect など多彩なメソッドがありますが、初学者がまず最初に身につけるべきはダントツで filter です。理由はシンプルで、業務コードでも学習用のサンプルでも、「あるリストから条件に合うものだけ抜き出す」という処理が圧倒的によく登場するからです。

たとえば「会員リストから 20 代だけ取り出す」「商品から在庫切れでないものだけ表示する」「ログから ERROR レベルのものだけ集計する」など、業務の現場で書かれる Java コードのかなりの割合が、突き詰めると「コレクションから条件に合う要素を残す」処理に行きつきます。

Java 入門コースでは for ループと if 文を使って同じことを実現してきました。Stream を覚えると、その「ループ + 条件分岐」のセットを 1 行で書けるようになります。コードが短くなるだけでなく、「何をしているか」がパッと読めるようになるのが最大のメリットです。

このレッスンでは、まず最頻出の filter だけにフォーカスして、Stream の基本的な書き方と「中間操作」と「終端操作」の関係を体に染み込ませていきます。mapreduce は次のレッスン以降で扱うので、ここでは「filter だけ」を徹底的に身につける気持ちで進めてください。

Stream パイプラインの全体像

Stream のコードは、おおまかに 3 つのパートに分かれます。

diagram (will load when visible)

左から「ソース」「中間操作」「終端操作」の順に並べ、最後に結果を取り出すというのが基本のかたちです。今回扱う filter は中間操作で、count は終端操作にあたります。中間操作は何個でもチェーンでつなげられますが、終端操作を呼ぶまでは実際の処理は始まりません (これを 遅延評価 といいます)。

「中間操作」と「終端操作」を区別するクセをつけると、Stream の API を覚えるのが一気に楽になります。中間操作は新しい Stream を返し、終端操作は intlong List などの「ふつうの値」を返す、と覚えてください。

filter(Predicate) の働き

filter は引数に Predicate<T> という関数型インタフェースを取ります。Predicate というのは「T を 1 つ受け取って boolean を返す関数」のことで、filter はこの関数が true を返した要素だけを次のステップに流します。

Java

import java.util.stream.IntStream; public class Demo { public static void main(String[] args) { int[] arr = {1, 2, 3, 4, 5, 6}; long even = IntStream.of(arr) .filter(n -> n % 2 == 0) .count(); System.out.println(even); // 3 } }

このコードでは n -> n % 2 == 0Predicate の実体です。Stream から 1 要素ずつ n に入れて条件を評価し、true の要素だけが count の対象になります。1 から 6 までの中で偶数は 2 4 6 の 3 個なので、出力は 3 です。

ラムダ式で条件を指定する

ラムダ式 n -> n % 2 == 0 は、Java の文法では次のような匿名クラスの省略形にあたります。

Java

IntPredicate evenChecker = new IntPredicate() { @Override public boolean test(int n) { return n % 2 == 0; } };

昔の Java ではこのように長々と書く必要がありましたが、Java 8 以降は n -> n % 2 == 0 という短い書き方ができるようになりました。ラムダ式の基本フォーマットは 引数 -> 式 で、引数の型は文脈から推論されるので省略できます。

複数行の処理を書きたい場合は、次のように { } でブロックを作って return を明示します。

Java

.filter(n -> { boolean isEven = n % 2 == 0; return isEven; })

ただし、filter の中で複数行のブロックを書くことはあまり推奨されません。判定ロジックが複雑になってきたら、後述する「メソッド参照」や「別メソッドへの切り出し」を検討するのが王道です。

ラムダ式は「コードをデータとして渡す」ための仕組みです。PythonlambdaJavaScript のアロー関数 n => n % 2 === 0 と発想は同じで、関数を変数のように扱えるのがポイントです。

メソッド参照との関係 (予告)

判定ロジックを別メソッドに切り出すと、filter の中身はさらに短くなります。

Java

public static boolean isEven(int n) { return n % 2 == 0; } // 呼び出し側 IntStream.of(arr) .filter(Solution::isEven) .count();

この Solution::isEven がメソッド参照です。「isEven というメソッド自体を Predicate として渡す」という意味で、n -> isEven(n) と同じ動きをします。

本格的な解説は別レッスンに譲りますが、filter の引数にはラムダ式だけでなくメソッド参照も渡せる、ということは頭の片隅に置いておいてください。テストしやすさやコードの読みやすさを考えると、業務コードではむしろメソッド参照の方が好まれる場面が多いです。

Stream のチェーン

filter の真価が発揮されるのは、複数の中間操作をチェーンしたときです。たとえば「偶数だけ抜き出して、それを 2 倍してから合計する」というのは、次のように書けます。

Java

int sum = IntStream.of(arr) .filter(n -> n % 2 == 0) .map(n -> n * 2) .sum();

中間操作 (filter map) は何回でも . でつなげられ、終端操作 (sum) を呼んだ瞬間に初めて全体が一気に評価されます。これが Stream の「パイプライン処理」の正体です。

diagram (will load when visible)

図のように、filter を通すと条件に合う要素だけが「残り」、count でその個数が long 値として取り出されます。filter 自体は配列や元のリストを書き換えないので、arr の中身は処理後も [1, 2, 3, 4, 5, 6] のままです。

よくある間違い

初学者が filter で踏みがちなトラップを 3 つ紹介します。

  • 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) を使うのが定石です

もう 1 つ細かい話として、IntStreamint のプリミティブを扱う特殊な Stream で、Stream<Integer> とは別物です。IntStream には sumaverage といった便利なメソッドがあるので、整数配列を扱うときは Arrays.stream(intArr)IntStream を作るのが王道です。

実務で遭遇するパターン

中級レッスンで学んだことが、実際の業務コードでどう登場するかを整理します。

  • バリデーション通過分の抽出users.stream().filter(User::isActive).toList()
  • ログから ERROR だけ抜き出すlines.filter(l -> l.contains("ERROR")).count()
  • 料金プランの絞り込みplans.filter(p -> p.price <= budget).filter(p -> p.region.equals("JP"))
  • カテゴリ別の集計の前段filter で対象を絞ってから groupingBy

コードレビューで指摘されがちなポイント

PR を出すとシニアから入りやすい指摘です。先回りで身につけておけばレビューが一発で通ります。

  • filter で副作用x -> { log.info(x); return x.ok; } のようなコードはレビューで NG
  • 負の条件で複雑になっているfilter(x -> !(x == null || x.value < 0))filter(x -> x != null && x.value >= 0) のほうが読みやすい
  • filter のあとに即 count ではなく collect ─ 件数だけ欲しいなら count で十分
  • Stream を再代入stream = stream.filter(...); のように変数を上書きするのは設計臭

パフォーマンス考慮事項

  • 遅延評価で短絡filter 後に findFirst を呼ぶと、最初の一致で止まる
  • Predicate.and / or ─ 複数条件を and(p2) で連結すると 1 個の filter で済む
  • 並列化と組み合わせparallelStream().filter(...) は CPU バウンドで効く
  • filter 後の size() は使えない ─ Stream は終端操作が必要、count() を使う
この章のポイント

ここまでの要点 stream.filter(Predicate) で条件通過のみ抽出。遅延評価で、findFirst などと組み合わせると無駄が少ない。

やってみよう

それでは今回の課題に進みましょう。やることは次のとおりです。

  1. Solution.evenCount(int[] arr) メソッドを完成させる
  2. Arrays.stream(arr) または IntStream.of(arr)IntStream を作る
  3. filter の引数にラムダ式を渡して、偶数 (n % 2 == 0) だけ残す
  4. 終端操作 count() で個数を long として取得し、int にキャストして return する

戻り値の型は int なので、count() が返す long をそのままでは返せません。return (int) stream.count(); のようにキャストするか、Math.toIntExact を使うのが安全です。今回のテストでは件数が数件しかないので、(int) でのキャストで十分です。

空配列 [] を渡したときは 0 を、奇数しかない [1, 3, 5] を渡したときも同じく 0 を返します。Stream は空でも例外を投げずに自然に 0 を返してくれるので、特別な if 文で先に判定する必要はありません。シンプルにそのまま書いてみてください。

慣れてきたら、filter の条件を n -> n > 3 (3 より大きい数の個数) に変えたり、map を挟んで偶数だけを 2 倍してから sum を取ったりと、いろいろ試してみると Stream の感覚がぐっと身につきます。次のレッスンでは、map を組み合わせて「絞り込んでから変換する」処理に挑戦していきます。

よくある質問

Q. filter と find の違いは?

A. filter は条件を満たす要素を全部集めて配列で返し、find は最初に見つかった 1 件だけ返します。1 件だけ欲しいなら find の方が短絡評価で速く、空のとき undefined(Python の next は StopIteration)になる点に注意してください。

Q. filter の戻り値が空配列のときの扱いは?

A. JS の filter は常に配列を返すため、空でも .length で安全にチェックできます。Python の filter はイテレータを返すので list() で囲んでから使うか、内包表記 [x for x in items if cond] の方が分かりやすいことが多いです。

Q. filter と map をチェーンしても性能は大丈夫ですか?

A. 通常規模なら問題ありません。配列を 2 周することになるため、要素数が数百万を超える場合だけ reduce にまとめるか、JS なら array.reduce で 1 周にすることを検討してください。可読性は filter→map のチェーンが分かりやすいです。

次のレッスン

次は map で変換する で、map で変換する を学びます。

事前確認 — 進む前に次の 3 つができることを確認しましょう。

  1. filter の要点を自分の言葉で説明できる
  2. このレッスンの最小コード (または操作手順) を見ずに書ける
  3. 練習問題やクイズで間違えた箇所を読み直して理解した

理解度チェック (30 秒)

Q. filter とは何か、1 文で説明してください。

この章のポイント

A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。

関連レッスン

要件

  1. Arrays.stream(arr) または IntStream.of(arr) を使って IntStream を作ること
  2. filter にラムダ式を渡して偶数 (n % 2 == 0) だけ残すこと
  3. 終端操作 count() の戻り値 (long) を int にキャストして返すこと

入出力例

test-cases.txt

evenCount([1,2,3,4,5,6])3 evenCount([])0 evenCount([1,3,5])0 evenCount([2,4,6,8])4 evenCount([0,-2,-3,7,-4])3

ヒント

main.java
main.java
学習モード

メモ

filter で絞り込む

⌘S で保存