【Java】Javaの関数型インターフェース(Function・Predicate等)の使い方

Java

こんにちは、かつコーチです。
ラムダ式を書けるようになったものの、「どの型に代入すればいいのか分からない」と迷った経験はありませんか。
今回は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>は「条件を満たすかどうか」を判定するときに使います。
後の記事で解説するStreamfilterメソッドは、この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にはandornegateFunctionにはandThencomposeという組み合わせ用のデフォルトメソッドが用意されています。
複数の条件や処理を1つずつ書かずに、部品として組み立てられるのがラムダ式ならではの強みです。

BiFunction・UnaryOperatorなど関連する型

引数が2つのBiFunction<T, U, R>や、入力と出力の型が同じUnaryOperator<T>など、派生した型も用意されています。
迷ったときは「入力の型・出力の型・引数の数」の3点で必要な型を絞り込むと選びやすくなります。

まとめ

この記事のポイント

  • 関数型インターフェースはラムダ式を代入するための型で、java.util.functionパッケージに代表例が用意されている
  • Functionは変換、Predicateは判定、Consumerは消費、Supplierは供給という役割の違いがある
  • 判定処理はFunction<T, Boolean>ではなくPredicate<T>を使うのが定石
  • andorandThenなどのデフォルトメソッドで処理を組み合わせられる

次に読むべき記事

  • ラムダ式の基本
  • Stream API基本

タグ: Java, 中級者向け, 関数型プログラミング

タイトルとURLをコピーしました