Java | Java 詳細・モダン文法:Stream API 深掘り – filter の設計

Java Java
スポンサーリンク

filter を一言でいうと

Stream#filter は、

「流れてくる要素のうち、“条件を満たすものだけ”を次のステップに通す中間操作」 です。

SQL でいう WHERE 句に近いイメージで、
true なら通す、false なら落とす――それだけを、ひたすら淡々とやる“門番”です。

この「門番の設計」をちゃんと意識できるようになると、
filter の書き方・分割の仕方・再利用の仕方が一気にうまくなります。


filter の基本形と Predicate の関係

シグネチャを正しく読む

filter のシグネチャはこうです。

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

ここで大事なのは 2 点です。

  1. 引数は Predicate<? super T>
  2. 戻り値は Stream<T>(中間操作)

Predicate<T>

@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);
}
Java

という関数型インターフェースで、
T を 1 つ受け取って、true/false を返す関数」= T -> boolean を表します。

つまり filter は、

T -> boolean な関数(条件)を 1 つ受け取り、
その条件が true になる要素だけを残した Stream<T> を返す」

というメソッドです。

一番シンプルな例

import java.util.List;

public class FilterBasic {
    public static void main(String[] args) {
        List<String> names = List.of("Alice", "Ai", "Bob", "Charlie");

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

        System.out.println(result); // [Alice, Bob, Charlie]
    }
}
Java

ここでの filter の条件は

name -> name.length() >= 3
Java

です。

name.length() >= 3true の要素だけが次に進み、
false の要素はそこで落とされます。


filter の「設計」を考える視点

1. 条件を“その場で書く”か、“メソッドに切り出す”か

最初はラムダをその場で書くので十分です。

.filter(user -> user.getAge() >= 20 && user.isActive())
Java

ただ、条件が長くなってきたら、
「名前を付けて外に出す」ことを考えます。

private static boolean isAdultActive(User u) {
    return u.getAge() >= 20 && u.isActive();
}

// 使う側
users.stream()
     .filter(FilterDesign::isAdultActive)
     .toList();
Java

こうすると、

「何をしているか」が isAdultActive という名前で一発で分かるし、
同じ条件を別の場所でも再利用できます。

2. Predicate を“部品”として設計する

条件をメソッドではなく、Predicate として持っておく設計もよく使います。

import java.util.function.Predicate;

Predicate<User> adult = u -> u.getAge() >= 20;
Predicate<User> active = User::isActive;

Predicate<User> adultAndActive = adult.and(active);
Predicate<User> minorOrInactive = adult.negate().or(active.negate());
Java

Predicate には

default Predicate<T> and(Predicate<? super T> other)
default Predicate<T> or(Predicate<? super T> other)
default Predicate<T> negate()
Java

が用意されていて、
条件を「AND」「OR」「NOT」で組み合わせられます。

これを filter に渡すと:

users.stream()
     .filter(adultAndActive)
     .toList();
Java

というふうに、「条件の組み立て」と「パイプラインの流れ」を分離できます。


filter の設計パターンを具体例で見る

例1:単純な条件をその場で書く

List<Integer> nums = List.of(1, 2, 3, 10, 20, 30);

List<Integer> result =
        nums.stream()
            .filter(n -> n > 10)
            .toList();

System.out.println(result); // [20, 30]
Java

「10 より大きい」という条件だけなら、
ラムダをその場で書くのが一番読みやすいです。

例2:複数条件をラムダ内で組み合わせる

List<Integer> result =
        nums.stream()
            .filter(n -> n > 10 && n < 30)
            .toList();
Java

&&|| で組み合わせるのはよくあるパターンです。

ただし、条件が複雑になってきたら、
「意味のまとまり」で分割した方が読みやすくなります。

例3:Predicate を使って条件を“部品化”する

import java.util.function.Predicate;

Predicate<Integer> greaterThan10 = n -> n > 10;
Predicate<Integer> lessThan30 = n -> n < 30;

Predicate<Integer> between10And30 = greaterThan10.and(lessThan30);

List<Integer> result =
        nums.stream()
            .filter(between10And30)
            .toList();
Java

between10And30 という名前が付くことで、
「何をしたい filter なのか」が一目で分かります。


ドメインオブジェクトでの filter 設計

例:User のリストから「20 歳以上・アクティブ・名前が A で始まる」ユーザーを抽出

まずはストレートにラムダで書いてみます。

List<User> filtered =
        users.stream()
             .filter(u -> u.getAge() >= 20
                       && u.isActive()
                       && u.getName().startsWith("A"))
             .toList();
Java

これでも動きますが、
条件が 1 行に詰まりすぎていて、読み手に優しくありません。

ここで「意味の単位」で分割します。

private static boolean isAdult(User u) {
    return u.getAge() >= 20;
}

private static boolean isActive(User u) {
    return u.isActive();
}

private static boolean nameStartsWithA(User u) {
    return u.getName().startsWith("A");
}
Java

使う側はこうなります。

List<User> filtered =
        users.stream()
             .filter(FilterDesign::isAdult)
             .filter(FilterDesign::isActive)
             .filter(FilterDesign::nameStartsWithA)
             .toList();
Java

あるいは Predicate でまとめてもいいです。

Predicate<User> adult = u -> u.getAge() >= 20;
Predicate<User> active = User::isActive;
Predicate<User> nameStartsWithA = u -> u.getName().startsWith("A");

Predicate<User> condition =
        adult.and(active).and(nameStartsWithA);

List<User> filtered =
        users.stream()
             .filter(condition)
             .toList();
Java

ここで大事なのは、

「filter の中に“全部詰め込む”のではなく、
条件を“名前の付いた部品”として設計する」

という発想です。


filter の“責務”を守る設計

filter の中で「副作用」を書かない

filter はあくまで「通すか落とすか」を決める門番です。

ここに「ログを書き込む」「外部サービスを呼ぶ」「状態を変更する」などの副作用を入れ始めると、
パイプラインの意味が一気に分かりにくくなります。

副作用を入れたいなら、
peekforEach に責務を分ける方がきれいです。

users.stream()
     .filter(User::isActive)
     .peek(u -> System.out.println("active: " + u.getName()))
     .toList();
Java

filter は「条件判定だけ」、
副作用は別のステップ――この線引きを意識しておくと、
Stream の設計がかなりスッキリします。


filter の設計を自分の中でこう定義しておく

最後に、filter の設計指針をあなたの言葉に落とすとこうなります。

「filter は、T -> boolean な条件(Predicate)を受け取り、
その条件を“名前の付いた部品”として設計し、
パイプラインの中で“何を通し、何を落とすか”を宣言する場所」

特に意識しておきたいのは、

中身は「純粋な条件判定」に徹すること
条件が複雑になったら、メソッドや Predicate に分割して名前を付けること
複数条件は and / or / negate で組み立てると再利用しやすいこと
filter の責務は「通すか落とすか」だけで、副作用は別ステップに逃がすこと

あたりです。

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