Java | Java 標準ライブラリ:filter 操作

Java Java
スポンサーリンク

filter 操作をざっくりイメージする

Stream の filter は、一言でいうと

「流れてくる要素の中から、条件に合うものだけを通し、合わないものを捨てる 操作」

です。

大事なポイントはこの 3 つです。

  • 要素数は「減る」ことはあるが、「増えない」
  • 1 つの要素について「残すか捨てるか」だけを決める
  • 要素の“形”や“型”は変えない(変えるのは map の役割)

for 文で書くときの

for (T x : list) {
    if (条件) {
        result.add(x);
    }
}
Java

この if (条件) の部分を、丸ごと filter に渡している、とイメージしてください。


一番基本:リストから偶数だけを取り出す

まずは for 文で書いてみる

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class FilterBasicFor {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);

        List<Integer> evens = new ArrayList<>();
        for (int n : numbers) {
            if (n % 2 == 0) {   // 偶数だけ残したい
                evens.add(n);
            }
        }

        System.out.println(evens);  // [2, 4, 6]
    }
}
Java

やっていることは単純で、

  • 1 個ずつ取り出す
  • 条件に合うものだけ evens に追加

という流れです。

同じことを Stream + filter で書く

import java.util.Arrays;
import java.util.List;

public class FilterBasicStream {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);

        List<Integer> evens =
                numbers.stream()
                       .filter(n -> n % 2 == 0)
                       .toList();   // Java 16 以降

        System.out.println(evens);  // [2, 4, 6]
    }
}
Java

日本語で読むと、

  • numbers.stream() でストリームを作り
  • .filter(n -> n % 2 == 0) で「偶数だけ通す」
  • .toList() で「通ったものを List に集める」

というパイプラインになっています。

「元の要素数 6 個 → 結果 3 個(2,4,6)」
つまり、filter は“間引く”操作だ、という感覚をはっきり持っておいてください。


filter の引数「Predicate」をちゃんと理解する

Predicate<T> は「T を受け取って、boolean を返す関数」

filter のシグネチャ(ざっくり)はこうです。

Stream<T> filter(Predicate<? super T> predicate)
Java

難しそうに見えますが、

  • Predicate<T> = 「T を受け取り、boolean を返す関数」

と思っておけば OK です。

なので、

.filter(n -> n % 2 == 0)
Java

は、Predicate<Integer> を 1 行のラムダ式で書いているだけです。
n を受け取って、n % 2 == 0 という真偽値を返す関数」。

true が返れば、その要素は“通過”
false が返れば、その要素は“捨てられる”

というルールです。

もう少し丁寧に書くと、こういう感じです。

Predicate<Integer> isEven = new Predicate<Integer>() {
    @Override
    public boolean test(Integer n) {
        return n % 2 == 0;
    }
};

numbers.stream()
       .filter(isEven)
       .toList();
Java

ラムダ式(n -> n % 2 == 0)は、この test メソッドの中身だけを簡単に書いている、と思ってください。


文字列に対するよくある filter の例

文字数で絞り込む

import java.util.Arrays;
import java.util.List;

public class FilterStringLength {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("Alice", "Bob", "Carol", "Dan");

        List<String> shortNames =
                names.stream()
                     .filter(name -> name.length() <= 3)
                     .toList();

        System.out.println(shortNames);  // [Bob, Dan]
    }
}
Java

name.length() <= 3 の部分が「条件(Predicate)」です。
ここを変えるだけで、いくらでも絞り込み条件を変えられます。

先頭文字で絞り込む

List<String> aNames =
        names.stream()
             .filter(name -> name.startsWith("A"))
             .toList();

System.out.println(aNames);  // [Alice]
Java

startsWith("A") は、「A で始まるかどうか」の boolean を返すメソッドです。
これをそのまま filter の条件に使っています。


filter と map の違いをきちんと分ける

filter は「通すか捨てるか」、map は「何に変えるか」

この 2 つを混同し始めると、一気にコードがぐちゃっとします。
感覚で整理するとこうです。

  • filter: 要素ごとに「残すか捨てるか」を決める。型・値は変えない。
  • map: 要素ごとに「変換結果」を作る。型や値を変える。

例えば、「偶数だけ 2 倍してリストにしたい」という処理。

for 文ならこう:

List<Integer> result = new ArrayList<>();
for (int n : numbers) {
    if (n % 2 == 0) {
        result.add(n * 2);
    }
}
Java

Stream ならこう:

List<Integer> result =
        numbers.stream()
               .filter(n -> n % 2 == 0) // 偶数だけ残す
               .map(n -> n * 2)         // 2倍に変換する
               .toList();
Java

流れを追うと、

元のストリーム:1, 2, 3, 4, 5
filter 後:2, 4
map 後:4, 8
結果の List:[4, 8]

という段階構成になっています。

filter → map
「絞ってから変える」

この順番は、Stream で処理を書くときの鉄板パターンです。


複数条件を使う filter(AND / OR / NOT)

AND 条件(かつ)で絞る

年齢 20 歳以上かつ 30 歳未満みたいな条件。

users.stream()
     .filter(user -> user.getAge() >= 20 && user.getAge() < 30)
     .toList();
Java

「&&」でつなげるだけです。
filter は 1 回きりでなく、複数回に分けても書けます。

users.stream()
     .filter(user -> user.getAge() >= 20)
     .filter(user -> user.getAge() < 30)
     .toList();
Java

どちらも結果は同じです。
後者の方が「20 歳以上」「30 歳未満」という 2 ステップがはっきり見えるので、条件が複雑になるほど読みやすくなります。

OR 条件(または)で絞る

「名前が Alice か Bob」のような条件:

names.stream()
     .filter(name -> name.equals("Alice") || name.equals("Bob"))
     .toList();
Java

「||」で OR 条件です。

NOT 条件(逆にする)

「null じゃないものだけ残す」など。

list.stream()
    .filter(Objects::nonNull)   // name != null と同じ意味
    .toList();
Java

Predicate には negate() などのメソッドもあるので、
慣れてきたら

Predicate<String> isEmpty = String::isEmpty;
list.stream()
    .filter(isEmpty.negate())   // 空でないものだけ
    .toList();
Java

のような「逆条件」もきれいに書けるようになります。


filter の中で「副作用」を書きすぎないこと

やってしまいがちなアンチパターン

初心者がやりがちな例:

List<String> log = new ArrayList<>();

List<String> result =
        names.stream()
             .filter(name -> {
                 boolean ok = name.length() <= 3;
                 if (ok) {
                     log.add(name);   // 外部リストを更新(副作用)
                 }
                 return ok;
             })
             .toList();
Java

技術的には動きますが、filter の中で「外部の状態を書き換える」と、
処理の意図がかなり分かりづらくなります。

  • filter は「残すか捨てるか」だけ決める
  • 何か“記録したい”なら、peekforEach など別の場所でやる

という役割分担を意識した方が、長期的に読みやすいコードになります。

filter の役割は「条件判定だけ」に絞る

理想的な filter の中身は、

  • if 文なしで、そのまま boolean を返す
  • 外部の変数を更新しない(副作用を持たない)

という状態です。

例えば:

.filter(user -> isPremium(user))   // 判定だけ
Java

のように、「条件を判断する関数(メソッド)を呼ぶだけ」にしておくと、
あとから読んだときに何をしているか一瞬で分かります。


filter をいつ使うか・いつ使わないか

使うべき場面

filter の出番は、とても多いです。

  • List の中から「条件を満たす要素だけの List」を作りたいとき
  • 集計の前に「対象を絞り込みたい」とき
  • map の前後で「範囲を狭めたい」とき

例えば、

  • 点数 80 点以上の受験者だけを集計する
  • ステータスが ACTIVE のユーザーだけを対象にする
  • null ではない値だけを次の処理に流す

こういった「対象を絞る」処理は全部 filter で書けます。

使わないほうがいい場面

逆に、次のようなケースでは filter は向きません。

  • 要素を“変換”したい(これは map の仕事)
  • 絞り込みつつ、「削除された要素をどこかに保存したい」など、副作用ゴリゴリの処理
  • インデックス情報(何番目か)も同時に見たい処理(この場合は for 文や IntStream.range のほうが分かりやすいことが多い)

「filter に何でも詰め込まない」「条件判定だけにする」
この線引きができると、Stream のコードは驚くほど読みやすくなります。


まとめ:filter 操作を自分の中でどう位置づけるか

filter を初心者向けに一言でまとめると、

要素を変えずに、“残すか捨てるか”だけを決める操作

です。

覚えておきたいポイントは次の通りです。

  • 引数は Predicate<T>T を受け取って boolean を返す関数)
  • true を返した要素だけが次の段階に流れ、false の要素は捨てられる
  • 要素数は減ることはあっても、増えることはない
  • 形や型を変えたいなら map、絞りたいなら filter
  • filter → map → collect(toList)の流れが、Stream の定番パターン

タイトルとURLをコピーしました