Predicate インターフェース
Predicate インターフェース とは
Java の関数型インターフェースの代表格
Predicate<T>を取り上げ、testメソッドで条件を判定し、andornegateで条件を合成する書き方を身につけよう。
Predicate<T> とはなにか
java.util.function.Predicate<T> は、Java の標準ライブラリにあらかじめ用意されている 関数型インターフェース のひとつです。直訳すると「述語」という意味で、平たく言うと「T 型の値を 1 つ受け取って true か false を返す関数」を表します。「正の数かどうか」「null でないかどうか」「文字列が Hello で始まるかどうか」といった、はい / いいえ で答えられる判定処理を、まるごとオブジェクトのように扱える仕組みです。
Java 入門コースでは、判定処理はだいたい
if (n > 0) { ... }のようにif文の中に直接書いてきました。Predicateを使うと、その判定ロジック自体をPredicate<Integer> isPositive = n -> n > 0;のように 1 個の変数として持ち回したり、他のメソッドに引数として渡したりできるようになります。これはStream APIやCollection.removeIfなど、現代の Java の標準ライブラリ全体で当たり前のように要求されてくる発想です。
Predicate<T> は、業務コードのなかでは Stream の filter の引数として登場することが圧倒的に多いですが、それ以外にも Optional.filter List.removeIf Stream.filter など、Java の標準 API のあちこちに顔を出します。最初に Predicate の作り方と合成の仕方をしっかり押さえておくと、これから出会う 関数型 API の習得スピードが一気に上がります。
インタフェース定義は驚くほどシンプル
Predicate<T> の中身は、ざっくりこんな形をしています。
Java
package java.util.function;
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
default Predicate<T> and(Predicate<? super T> other) { ... }
default Predicate<T> or(Predicate<? super T> other) { ... }
default Predicate<T> negate() { ... }
static <T> Predicate<T> isEqual(Object targetRef) { ... }
}ポイントは、抽象メソッドが test(T t) の 1 個だけで、戻り値が boolean になっているところです。@FunctionalInterface というアノテーションは「抽象メソッドが 1 個だけのインタフェース」という宣言で、これがあるとラムダ式やメソッド参照で実装できる、というのが Java 8 以降のルールです。
default修飾子がついたandornegateは、インタフェースなのに実装が書かれているメソッドです。Java 8 で導入された仕組みで、既存のインタフェースに後からメソッドを追加しても、実装側が壊れないように作られています。利用する側は、Predicateを作るだけでandornegateが無料でついてくる、という気持ちで使えば十分です。
ラムダ式で Predicate を作る
Predicate<T> の本体は test メソッド 1 個なので、ラムダ式で簡単に実装できます。たとえば「正の数かどうか」を判定する Predicate<Integer> は、たった 1 行で書けます。
Java
import java.util.function.Predicate;
public class Demo {
public static void main(String[] args) {
Predicate<Integer> isPositive = n -> n > 0;
System.out.println(isPositive.test(5)); // true
System.out.println(isPositive.test(0)); // false
System.out.println(isPositive.test(-3)); // false
}
}ここで使った n -> n > 0 がラムダ式で、左側の n が test メソッドの引数、右側の n > 0 が boolean の戻り値にあたります。型 (Integer) は左辺の Predicate<Integer> から自動で推論されるので、わざわざ (Integer n) -> n > 0 と書く必要はありません。
ラムダ式の正体は「
testメソッドだけを実装した匿名クラスの省略形」です。Java 7 までの古い書き方では、new Predicate<Integer>() { @Override public boolean test(Integer n) { return n > 0; } }のように長々と書く必要がありました。Java 8 以降は、抽象メソッドが 1 個ならラムダ式に丸ごと置き換えられます。
書き終わった Predicate は普通の変数のように扱えるので、メソッドの引数として渡したり、List に入れて使い分けたり、自由に取り回せます。これが「関数をオブジェクトとして扱う」、いわゆる 関数型プログラミング の入り口です。
test メソッドの戻り値は必ず boolean
ここで強調しておきたいのは、Predicate<T>.test(T) の戻り値は boolean 固定で、それ以外の型は返せないということです。Predicate<String> を作ったからといって String を返せるわけではありません。「String を受け取って boolean を返す関数」というのが、Predicate<String> の意味です。
Java
Predicate<String> isLong = s -> s.length() >= 10;
Predicate<String> isShort = s -> s.length() < 3;
Predicate<String> isEmpty = String::isEmpty;
System.out.println(isLong.test("Hello, World!")); // true
System.out.println(isShort.test("hi")); // true
System.out.println(isEmpty.test("")); // true最後の String::isEmpty がメソッド参照で、s -> s.isEmpty() と同じ意味です。String クラスにもとから定義されている isEmpty() メソッドは「自分自身が空文字なら true を返す」という、まさに Predicate<String> の形なので、そのまま Predicate として渡せます。
and or negate で合成する
Predicate のうれしいところは、and or negate を使って複数の条件を合成できる点です。たとえば「正の数かつ偶数」という条件は、こう書けます。
Java
Predicate<Integer> isPositive = n -> n > 0;
Predicate<Integer> isEven = n -> n % 2 == 0;
Predicate<Integer> positiveAndEven = isPositive.and(isEven);
Predicate<Integer> positiveOrEven = isPositive.or(isEven);
Predicate<Integer> notPositive = isPositive.negate();
System.out.println(positiveAndEven.test(4)); // true
System.out.println(positiveAndEven.test(3)); // false (奇数なので)
System.out.println(positiveOrEven.test(-2)); // true (偶数なので)
System.out.println(notPositive.test(-5)); // true (正でないので)and は &&、or は ||、negate は ! に対応すると考えると分かりやすいです。違いは、これらが普通の演算子ではなく メソッド なので、isPositive.and(isEven) のように . でつなぐ点と、結果が新しい Predicate<T> として返ってくる点です。
図のように、いったん基本となる Predicate を 1 つずつ作っておけば、あとは and or negate で組み立てるだけで複雑な条件を表現できます。if (n > 0 && n % 2 == 0) を if 文に直接書くより、positiveAndEven.test(n) と書いた方が「何の条件で判定しているか」が読み手にひと目で伝わります。
Stream.filter との関係
Predicate<T> が一番輝くのは、Stream API と組み合わせたときです。Stream<T>.filter の引数は実は Predicate<? super T> なので、自分で作った Predicate をそのまま渡せます。
Java
import java.util.List;
import java.util.function.Predicate;
public class Demo {
public static void main(String[] args) {
Predicate<Integer> isPositive = n -> n > 0;
long count = List.of(-1, 2, -3, 4, 5).stream()
.filter(isPositive)
.count();
System.out.println(count); // 3
}
}filter に直接ラムダ式を書くのではなく、isPositive という名前のついた Predicate を渡しているのがポイントです。条件に名前がつくとコードの意図が明確になりますし、同じ条件をあちこちで使い回せるので保守もしやすくなります。
今回の課題は、まさにこの「Predicate を変数として作っておき、filter に渡して数を数える」というパターンです。Stream API を本格的に使いこなす最初の一歩なので、ここで Predicate と filter の関係をしっかり腹落ちさせておいてください。
よくある間違い
Predicate を使い始めたばかりのときに踏みやすい落とし穴を 3 つ紹介します。事前に知っておくだけでエラーで悩む時間が大幅に減ります。
testの戻り値型を勘違いする —Predicate<Integer>を作るときに、ついn -> n * 2のような「整数を返すラムダ」を書いてしまうケースです。Predicate.testは必ずbooleanを返す約束なので、n -> n * 2はPredicate<Integer>としては成立せず、incompatible typesというコンパイルエラーになります。整数を別の整数に変換したいときはPredicateではなくFunction<Integer, Integer>を使うのが正解ですnull判定を手書きしすぎる — 参照型を扱うPredicate<String>などでは、要素にnullが混ざることがあります。素直にs -> s != null && s.length() > 0と書いてもよいのですが、nullチェック専用のメソッド参照Objects::nonNullがjava.util.Objectsに用意されています。Predicate<String> notNull = Objects::nonNull;のように書くと、意図が一発で伝わるのでおすすめですnegateの順序を間違える —isPositive.and(isEven).negate()とisPositive.and(isEven.negate())は、見た目は似ていますが意味がまるで違います。前者は「正かつ偶数」の否定 (=負または奇数)、後者は「正かつ奇数」です。negateがどのPredicateにかかっているかを意識して、必要なら(...)で明示するか、いったん変数に入れて段階的に組み立てるのが安全です
もう 1 つ細かい注意点として、
Predicate<int>とは書けないことが挙げられます。ジェネリクスはプリミティブ型を受け取れない仕様なので、整数を扱いたいときはPredicate<Integer>のようにラッパー型を使うか、int専用のIntPredicateを使い分けてください。
やってみよう
それでは今回の課題に挑戦しましょう。やることは次のとおりです。
Solution.positiveCount(int[] arr)メソッドを完成させる- メソッドの中で
Predicate<Integer> isPositive = n -> n > 0;を作る - 配列を
for-eachかStreamで回し、isPositive.test(n)がtrueの個数を数える - その個数を
intとしてreturnする
例として、positiveCount(new int[]{-1, 2, -3, 4, 5}) は正の数が 2 4 5 の 3 つなので 3 を返します。空配列 new int[]{} や new int[]{0} のように正の数が 1 つもない配列では 0 を返します。0 は「正の数」ではないので数えない、という点に注意してください (n > 0 であって n >= 0 ではありません)。
書き方は 2 通りあります。for-each を使うとこんな感じです。
Java
import java.util.function.Predicate;
public class Solution {
public static int positiveCount(int[] arr) {
Predicate<Integer> isPositive = n -> n > 0;
int count = 0;
for (int n : arr) {
if (isPositive.test(n)) {
count++;
}
}
return count;
}
}Stream を使うともう少し短く書けます。
Java
import java.util.Arrays;
import java.util.function.Predicate;
public class Solution {
public static int positiveCount(int[] arr) {
Predicate<Integer> isPositive = n -> n > 0;
return (int) Arrays.stream(arr).boxed()
.filter(isPositive)
.count();
}
}どちらの書き方でも構いません。今回の本題は「Predicate を変数として作って test を呼ぶ」というところなので、まずは for-each 版でしっかり動かしてみるのがおすすめです。慣れてきたら Stream 版に書き直したり、isPositive.and(isEven) を作って「正の偶数の個数」を数える練習に拡張してみてください。次のレッスンでは、Function<T, R> や Consumer<T> といった他の関数型インターフェースを学んで、さらに表現の幅を広げていきます。
よくある質問
Q. abstract クラスと interface はどう使い分けますか?
A. 型の契約だけ定義するなら interface、共通実装も含めて部分的に書きたいなら abstract クラスです。Java 8 以降は interface も default メソッドで実装を持てるため、abstract の出番は減っていますが、フィールド共有が必要なら abstract が必要です。
Q. interface に変数を書けますか?
A. 暗黙的に public static final な定数だけ書けます。ミュータブルなフィールドは持てないため、状態を持たせたい場合は abstract クラスにしてください。定数置き場として使う場合も、専用クラス + private コンストラクタの方がベターな設計です。
Q. 1 つのクラスに複数の interface を実装できますか?
A. できます。implements A, B, C のようにカンマ区切りで列挙します。複数の default メソッドが同名で衝突した場合はコンパイルエラーになるため、override で明示的に解決します。多重継承の難しさをこの形で限定的に解放しているのが Java の設計です。
次のレッスン
次は Consumer と Supplier で、Consumer と Supplier を学びます。
事前確認 — 進む前に次の 3 つができることを確認しましょう。
- Predicate の要点を自分の言葉で説明できる
- このレッスンの最小コード (または操作手順) を見ずに書ける
- 練習問題やクイズで間違えた箇所を読み直して理解した
理解度チェック (30 秒)
Q. Predicate とは何か、1 文で説明してください。
A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。
関連レッスン
要件
- メソッドの中で
Predicate<Integer> isPositive = n -> n > 0;をラムダ式で作ること - 配列の各要素に対して
isPositive.test(n)を呼び、trueのものだけ数えること (if (n > 0)の直書きはせず、必ずPredicateを経由する) - ファイル先頭で
import java.util.function.Predicate;を書くこと
入出力例
test-cases.txt
positiveCount([-1,2,-3,4,5]) → 3
positiveCount([]) → 0
positiveCount([0]) → 0
positiveCount([-1,-2,-3]) → 0
positiveCount([1,2,3,4]) → 4
positiveCount([0,1,0,2,0,3]) → 3