Java Stream filter応用ガイド!複数条件・否定条件・Predicate合成を徹底解説
生徒
「JavaのStream APIでfilterを使っているのですが、条件が2つ以上ある場合や、逆に『~ではないもの』だけを抽出したい時はどうすればいいですか?」
先生
「それは非常に重要なテクニックですね。Streamのfilterメソッドは、論理演算子を使ったり、Predicateという仕組みを組み合わせたりすることで、複雑な条件もスッキリ記述できるんですよ。」
生徒
「Predicateの合成とか難しそうですが、初心者でも書けるようになりますか?」
先生
「もちろんです!まずは基本的な複数条件の書き方から、徐々に応用的な再利用の方法まで、具体例を交えて順番に学んでいきましょう。」
1. Stream filterの基本と複数条件の指定方法
JavaのStream APIにおいて、filterメソッドは要素を絞り込むための中心的な役割を果たします。初心者の皆さんが最初に直面するのが、「AかつB」や「AまたはB」といった複数の条件を指定したい場面です。
最も直感的で簡単な方法は、ラムダ式の中で論理演算子(&& や ||)を使用することです。これにより、一つのfilterメソッドの中で複数の判定ロジックを完結させることができます。例えば、数値のリストから「10以上」かつ「50以下」の数字だけを取り出したい場合、数学的な範囲指定をそのままコードに落とし込むことが可能です。
この方法はコードの記述量が少なく、小規模な処理であれば非常に読みやすいというメリットがあります。まずはこの基本形をしっかりとマスターしましょう。Javaのプログラミングでは、いかに読みやすく保守しやすいコードを書くかが重要視されますが、filterの中身が複雑になりすぎないように注意することも大切です。
2. 論理演算子を使った複数条件のサンプルコード
それでは、具体的に論理演算子を用いた複数条件のフィルタリングを見ていきましょう。ここでは、商品のリストから特定の価格帯かつ特定のカテゴリーに属するものだけを抽出する例を紹介します。
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class StreamFilterExample {
public static void main(String[] args) {
List<String> fruits = Arrays.asList("Apple", "Banana", "Cherry", "Apricot", "Blueberry");
// 「A」から始まり、かつ文字数が5文字以上のものを抽出
List<String> result = fruits.stream()
.filter(f -> f.startsWith("A") && f.length() >= 5)
.collect(Collectors.toList());
System.out.println("フィルタリング結果: " + result);
}
}
上記のコードを実行すると、次のような結果が得られます。条件に合致する「Apple」と「Apricot」が抽出されていることがわかります。
フィルタリング結果: [Apple, Apricot]
3. 否定条件の書き方と注意点
次に、特定の条件に「一致しないもの」を除外したい場合に使う「否定条件」について解説します。Javaでは否定演算子(!)を使用します。例えば、「空文字ではないもの」や「特定のフラグが立っていないデータ」を抽出する際に多用されます。
filterの中では .filter(s -> !s.isEmpty()) のように記述します。しかし、否定条件が増えてくると、二重否定のようになってしまい、コードの可読性が低下することがあります。「~ではない、かつ、~ではない」という条件を書くときは、後述するPredicateメソッドを使うことで、より自然な英語に近い形で記述できるようになります。
また、Java 11からは Predicate.not() という便利なメソッドが追加されました。これを使うことで、メソッド参照を利用しながら否定条件をスマートに書くことができます。初心者の方も、最新のJavaの書き方に慣れておくと、現場で重宝されるスキルになります。
4. filterを重ねて書くことによる可読性の向上
複数条件を指定するもう一つの方法は、filterメソッドをチェーン(連結)させることです。一つのfilterの中に長い論理式を書くのではなく、一つの条件ごとにfilterを分ける手法です。
この書き方の最大のアドバンテージは、各条件が独立しているため、デバッグがしやすくなる点です。例えば、1つ目のfilterで何件残っているか、2つ目のfilterで何件まで絞られたかを追いやすくなります。内部的なパフォーマンスについても、Javaの最適化処理によって大きな差が出ないことが多いため、読みやすさを優先して分割して書くことが推奨されるケースも多いです。
特に「AかつBかつC」というAND条件の場合は、この連結スタイルが非常に効果的です。一方で「OR(または)」条件の場合はfilterを分けることができないため、その点は注意が必要です。状況に応じて適切な書き方を選べるようになりましょう。
import java.util.Arrays;
import java.util.List;
public class StreamChainExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(10, 15, 20, 25, 30, 35, 40);
// filterを重ねて記述(AND条件と同じ効果)
numbers.stream()
.filter(n -> n > 20) // 20より大きい
.filter(n -> n % 10 == 0) // 10の倍数
.forEach(System.out::println);
}
}
30
40
5. Predicateインターフェースによる条件の変数化
より高度な応用として、条件そのものを変数として定義する方法があります。Javaでは、条件を判定するロジックを Predicate<T> という型の変数に代入できます。これにより、同じ条件を複数の場所で再利用したり、条件に名前を付けて分かりやすくしたりすることが可能になります。
例えば Predicate<String> isRich = s -> s.length() > 10; のように定義しておけば、filterの中には .filter(isRich) と書くだけで済みます。何をしている条件なのかが一目でわかるため、大規模な開発現場では非常に好まれる書き方です。
Predicateを使うことで、関数の引数としてフィルタリング条件を渡すといった柔軟なプログラミングも可能になります。これは関数型プログラミングの考え方を取り入れたJava Stream APIの真骨頂とも言える部分です。少しずつ慣れていきましょう。
6. Predicateの合成による複雑な条件構築
Predicateには、複数の条件を論理的に結合するための専用メソッドが用意されています。具体的には and()、or()、negate() の3つです。これらを組み合わせることで、複雑な条件をパズルのように組み立てることができます。
例えば、「条件A または (条件B かつ 条件Cの否定)」といった複雑な条件も、メソッドチェーンで表現できます。この方法の素晴らしい点は、ラムダ式の中に直接ロジックを書くよりも、文章のように読み進められる点にあります。条件が動的に変わるようなシステム(画面の検索条件によって絞り込みを増やす等)を作る際には、このPredicate合成が威力を発揮します。
初心者のうちは難しく感じるかもしれませんが、「条件を部品として作る」というイメージを持つことが上達の近道です。部品を組み合わせることで、バグが少なくメンテナンス性の高いプログラムが出来上がります。
7. Predicate合成の実践サンプルコード
ここでは実際に、Predicateの and と negate を使った応用例を見てみましょう。従業員リストから、特定の役職であり、かつ特定の部署に所属していない人を抽出するようなイメージです。
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
public class PredicateCompositeExample {
public static void main(String[] args) {
List<String> names = Arrays.asList("Tanaka", "Sato", "Suzuki", "Ito", "Watanabe");
// 条件1: 4文字以上
Predicate<String> longName = s -> s.length() >= 4;
// 条件2: 'S'で始まる
Predicate<String> startsWithS = s -> s.startsWith("S");
// 4文字以上、かつ 'S'で始まらないものを抽出
names.stream()
.filter(longName.and(startsWithS.negate()))
.forEach(System.out::println);
}
}
このコードでは、まず名前の長さをチェックし、その後に「Sで始まる」という条件を反転(negate)させて結合しています。出力結果は以下の通りです。
Tanaka
Ito
Watanabe
8. Optionalと組み合わせたfilterの活用
Stream APIだけでなく、Java 8から導入された Optional クラスでも filter メソッドが使えます。これは、値が存在する場合にのみ条件をチェックし、条件に合致しなければ空のOptionalを返すという動きをします。
リストの処理だけでなく、単一のオブジェクトに対する条件判定においてもfilterの考え方は応用できます。例えば、ユーザー情報が取得できた場合に、そのユーザーが管理者権限を持っているかどうかをチェックする処理を、if文を使わずにスマートに記述できます。Streamで学んだ複数条件や否定条件の知識は、そのままOptionalでも役立つため、Java全体のコーディングスキル向上に直結します。
if文のネスト(階層)が深くなって困っている方は、ぜひこのfilterとOptionalの組み合わせを検討してみてください。コードが驚くほどスッキリし、意図が伝わりやすい美しいプログラムへと進化します。
9. パフォーマンスと例外処理に関するベストプラクティス
最後に、実務で役立つ一歩進んだ注意点をお伝えします。filterの中でチェックする条件が重い処理(データベースへの問い合わせや外部APIの呼び出しなど)を含む場合、実行順序に注意が必要です。軽い条件から先にfilterをかけることで、重い処理の実行回数を減らすことができます。
また、filterの中でチェック例外が発生するメソッドを呼び出したい場合、ラムダ式内では直接例外を投げることができないため、ラップするなどの工夫が必要になります。こうした実務上の課題にぶつかった時も、今回学んだPredicateの知識があれば、独自にエラーハンドリングを組み込んだカスタムPredicateを作成して対応できるようになります。
Stream APIは非常に強力ですが、魔法の杖ではありません。原理原則を理解し、適切な場面で適切な条件指定を行うことで、Javaエンジニアとしてのレベルが一段階アップします。まずは小さなコードから、今回紹介した複数条件やPredicate合成を試してみてください。
import java.util.stream.Stream;
public class AdvancedFilterTip {
public static void main(String[] args) {
// nullチェックと文字数チェックを組み合わせる安全な書き方
Stream.of("Java", null, "Stream", "API")
.filter(s -> s != null && s.length() > 3)
.forEach(System.out::println);
}
}
Java
Stream