こんにちは、かつコーチです。
ラムダ式を書けるようになったものの、「どの型に代入すればいいのか分からない」と迷った経験はありませんか。
今回はJavaが標準で用意している関数型インターフェース(Function・Predicate・Consumer・Supplier)の役割と使い分けを解説します。
関数型インターフェースとは?
なぜ標準の型が用意されているのか
前回の記事で、ラムダ式は抽象メソッドを1つだけ持つ関数型インターフェースにしか代入できないと説明しました。
毎回自分で@FunctionalInterfaceを定義するのは手間なので、Javaはjava.util.functionパッケージに代表的なパターンをあらかじめ用意しています。
「引数を受け取って値を返す」「判定する」「受け取るだけ」「作るだけ」という4つの型を覚えておくと、ほとんどの場面をカバーできます。
代表的な4つの型
| インターフェース | 抽象メソッド | 役割 |
|---|---|---|
Function<T, R> | R apply(T t) | T型を受け取りR型を返す(変換) |
Predicate<T> | boolean test(T t) | T型を受け取りtrue/falseを返す(判定) |
Consumer<T> | void accept(T t) | T型を受け取るだけで何も返さない(消費) |
Supplier<T> | T get() | 何も受け取らずT型を返す(供給) |
基本の書き方
Function:変換する
import java.util.function.Function;
Function<String, Integer> toLength = s -> s.length();
System.out.println(toLength.apply("かつコーチ")); // 5
Function<T, R>は「Tを受け取ってRを返す」という変換処理を表します。
型パラメータの1つ目が入力、2つ目が出力である点を意識すると読みやすくなります。
Predicate:判定する
import java.util.function.Predicate;
Predicate<Integer> isEven = n -> n % 2 == 0;
System.out.println(isEven.test(4)); // true
System.out.println(isEven.test(7)); // false
Predicate<T>は「条件を満たすかどうか」を判定するときに使います。
後の記事で解説するStreamのfilterメソッドは、このPredicateを引数に取ります。
Consumer:受け取って処理する
import java.util.function.Consumer;
Consumer<String> printer = s -> System.out.println("受信: " + s);
printer.accept("Javaの学習中です"); // 受信: Javaの学習中です
Consumer<T>は値を受け取るだけで、戻り値がありません。
ログ出力や画面表示など、「受け取った値に対して何かする」処理に向いています。
Supplier:値を作る
import java.util.function.Supplier;
Supplier<String> idGenerator = () -> "ID-" + System.currentTimeMillis();
System.out.println(idGenerator.get()); // 例: ID-1735600000000
Supplier<T>は引数を取らず、呼ばれるたびに値を「供給」します。
遅延評価(実際に必要になるまで処理を先延ばしにすること)と組み合わせて使われることが多いインターフェースです。
よくあるつまずきポイント・エラー対処
FunctionとPredicateを混同してコンパイルエラーになった
実際に私がやってしまった間違いですが、判定処理のつもりでFunction<T, Boolean>を使い、後からPredicateに書き換える手間が発生したことがあります。
// ❌Before:判定なのにFunction<T, Boolean>を使う
Function<Integer, Boolean> isPositive = n -> n > 0;
if (isPositive.apply(5)) {
System.out.println("正の数です");
}
動作はしますが、filterなどPredicateを要求するメソッドにそのまま渡せず、意図も伝わりにくいコードになります。
// ✅After:判定処理はPredicateを使う
Predicate<Integer> isPositive = n -> n > 0;
if (isPositive.test(5)) {
System.out.println("正の数です");
}
「trueかfalseを返す判定処理」はFunction<T, Boolean>ではなくPredicate<T>を使うのが定石です。
Stream APIとの連携も考えると、用途に合った型を最初から選ぶことが重要です。
応用・一歩先の使い方
PredicateやFunctionを組み合わせる
Predicate<Integer> isEven = n -> n % 2 == 0;
Predicate<Integer> isPositive = n -> n > 0;
Predicate<Integer> isPositiveEven = isEven.and(isPositive);
System.out.println(isPositiveEven.test(4)); // true
System.out.println(isPositiveEven.test(-4)); // false
Function<Integer, Integer> square = n -> n * n;
Function<Integer, Integer> addOne = n -> n + 1;
Function<Integer, Integer> squareThenAddOne = square.andThen(addOne);
System.out.println(squareThenAddOne.apply(3)); // 10(3の2乗9に1を足す)
Predicateにはand・or・negate、FunctionにはandThen・composeという組み合わせ用のデフォルトメソッドが用意されています。
複数の条件や処理を1つずつ書かずに、部品として組み立てられるのがラムダ式ならではの強みです。
BiFunction・UnaryOperatorなど関連する型
引数が2つのBiFunction<T, U, R>や、入力と出力の型が同じUnaryOperator<T>など、派生した型も用意されています。
迷ったときは「入力の型・出力の型・引数の数」の3点で必要な型を絞り込むと選びやすくなります。
まとめ
この記事のポイント
- 関数型インターフェースはラムダ式を代入するための型で、
java.util.functionパッケージに代表例が用意されている Functionは変換、Predicateは判定、Consumerは消費、Supplierは供給という役割の違いがある- 判定処理は
Function<T, Boolean>ではなくPredicate<T>を使うのが定石 and・or・andThenなどのデフォルトメソッドで処理を組み合わせられる
次に読むべき記事
- ラムダ式の基本
- Stream API基本
タグ: Java, 中級者向け, 関数型プログラミング