filter を一言でいうと
Stream#filter は、
「流れてくる要素のうち、“条件を満たすものだけ”を次のステップに通す中間操作」 です。
SQL でいう WHERE 句に近いイメージで、true なら通す、false なら落とす――それだけを、ひたすら淡々とやる“門番”です。
この「門番の設計」をちゃんと意識できるようになると、
filter の書き方・分割の仕方・再利用の仕方が一気にうまくなります。
filter の基本形と Predicate の関係
シグネチャを正しく読む
filter のシグネチャはこうです。
Stream<T> filter(Predicate<? super T> predicate)
Javaここで大事なのは 2 点です。
- 引数は
Predicate<? super T> - 戻り値は
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() >= 3 が true の要素だけが次に進み、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());
JavaPredicate には
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();
Javabetween10And30 という名前が付くことで、
「何をしたい 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 はあくまで「通すか落とすか」を決める門番です。
ここに「ログを書き込む」「外部サービスを呼ぶ」「状態を変更する」などの副作用を入れ始めると、
パイプラインの意味が一気に分かりにくくなります。
副作用を入れたいなら、peek や forEach に責務を分ける方がきれいです。
users.stream()
.filter(User::isActive)
.peek(u -> System.out.println("active: " + u.getName()))
.toList();
Javafilter は「条件判定だけ」、
副作用は別のステップ――この線引きを意識しておくと、
Stream の設計がかなりスッキリします。
filter の設計を自分の中でこう定義しておく
最後に、filter の設計指針をあなたの言葉に落とすとこうなります。
「filter は、T -> boolean な条件(Predicate)を受け取り、
その条件を“名前の付いた部品”として設計し、
パイプラインの中で“何を通し、何を落とすか”を宣言する場所」
特に意識しておきたいのは、
中身は「純粋な条件判定」に徹すること
条件が複雑になったら、メソッドや Predicate に分割して名前を付けること
複数条件は and / or / negate で組み立てると再利用しやすいこと
filter の責務は「通すか落とすか」だけで、副作用は別ステップに逃がすこと
あたりです。
