filter で絞り込む
このレッスンで分かること
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 の基本的な書き方と「中間操作」と「終端操作」の関係を体に染み込ませていきます。map や reduce は次のレッスン以降で扱うので、ここでは「filter だけ」を徹底的に身につける気持ちで進めてください。
Stream パイプラインの全体像
Stream のコードは、おおまかに 3 つのパートに分かれます。
左から「ソース」「中間操作」「終端操作」の順に並べ、最後に結果を取り出すというのが基本のかたちです。今回扱う filter は中間操作で、count は終端操作にあたります。中間操作は何個でもチェーンでつなげられますが、終端操作を呼ぶまでは実際の処理は始まりません (これを 遅延評価 といいます)。
「中間操作」と「終端操作」を区別するクセをつけると、
Streamの API を覚えるのが一気に楽になります。中間操作は新しいStreamを返し、終端操作はintやlongListなどの「ふつうの値」を返す、と覚えてください。
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 == 0 が Predicate の実体です。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 の中で複数行のブロックを書くことはあまり推奨されません。判定ロジックが複雑になってきたら、後述する「メソッド参照」や「別メソッドへの切り出し」を検討するのが王道です。
ラムダ式は「コードをデータとして渡す」ための仕組みです。
PythonのlambdaやJavaScriptのアロー関数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 の「パイプライン処理」の正体です。
図のように、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 つ細かい話として、
IntStreamはintのプリミティブを扱う特殊なStreamで、Stream<Integer>とは別物です。IntStreamにはsumやaverageといった便利なメソッドがあるので、整数配列を扱うときは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 などと組み合わせると無駄が少ない。
やってみよう
それでは今回の課題に進みましょう。やることは次のとおりです。
Solution.evenCount(int[] arr)メソッドを完成させるArrays.stream(arr)またはIntStream.of(arr)でIntStreamを作るfilterの引数にラムダ式を渡して、偶数 (n % 2 == 0) だけ残す- 終端操作
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 つができることを確認しましょう。
- filter の要点を自分の言葉で説明できる
- このレッスンの最小コード (または操作手順) を見ずに書ける
- 練習問題やクイズで間違えた箇所を読み直して理解した
理解度チェック (30 秒)
Q. filter とは何か、1 文で説明してください。
A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。
関連レッスン
要件
Arrays.stream(arr)またはIntStream.of(arr)を使ってIntStreamを作ることfilterにラムダ式を渡して偶数 (n % 2 == 0) だけ残すこと- 終端操作
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