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]
}
}
Javaname.length() <= 3 の部分が「条件(Predicate)」です。
ここを変えるだけで、いくらでも絞り込み条件を変えられます。
先頭文字で絞り込む
List<String> aNames =
names.stream()
.filter(name -> name.startsWith("A"))
.toList();
System.out.println(aNames); // [Alice]
JavastartsWith("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);
}
}
JavaStream ならこう:
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();
JavaPredicate には 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 は「残すか捨てるか」だけ決める
- 何か“記録したい”なら、
peekやforEachなど別の場所でやる
という役割分担を意識した方が、長期的に読みやすいコードになります。
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 の定番パターン
