概要

java.util.function パッケージには Function、Consumer、Supplier、Predicate をはじめとする汎用の関数型インターフェースが揃っています。Stream API を使い始めると自然に触れることになりますが、「Function と UnaryOperator の違いは何か」「Consumer をどこで使うのか」「BiFunction はいつ出番があるのか」といった疑問が実務では頻繁に出てきます。この記事では、各インターフェースの入力・出力の型パターンを整理したうえで、商品データの変換・フィルタリング・割引計算・ログ出力といった業務に近い場面での使い方を示します。Java 17 では record と組み合わせることでデータ変換パイプラインが読みやすくなり、Java 21 では sealed interface による型安全な操作定義という選択肢も加わります。各インターフェースの関係を把握しておくと、Stream の中間操作や終端操作に渡すラムダ式の設計が格段にしやすくなります。

使いどころ

データベースから取得した Entity を画面表示用の DTO へ変換する Function<Entity, Dto> を定義し、Stream.map に渡す

バッチ処理の各ステップで処理結果をログ出力する Consumer<StepResult> を定義し、forEach や peek で適用する

商品検索の絞り込み条件を Predicate<Product> として組み立て、and/or/negate で動的にフィルタを構成する

コード例

FunctionInterfaceDemo.java
import java.util.List;
import java.util.function.*;

public class FunctionInterfaceDemo {

    record Product(String name, int price) {}

    public static void main(String[] args) {

        Function<String, Integer> strLength = String::length;
        Function<Integer, String> intToStr = n -> "文字数: " + n;
        var combined = strLength.andThen(intToStr);
        System.out.println(combined.apply("Java")); // 文字数: 4

        var products = List.of(
            new Product("ノートPC", 80000),
            new Product("マウス", 3000),
            new Product("キーボード", 12000)
        );
        Consumer<Product> printProduct = p ->
            System.out.println(p.name() + ": " + p.price() + "円");
        products.forEach(printProduct);

        Supplier<List<String>> defaults = () -> List.of("未設定");
        System.out.println(defaults.get()); // [未設定]

        Predicate<Product> isExpensive = p -> p.price() >= 10000;
        Predicate<Product> isCheap = isExpensive.negate();

        System.out.println("高額商品:");
        products.stream()
            .filter(isExpensive)
            .map(Product::name)
            .forEach(name -> System.out.println("  " + name));

        System.out.println("低価格商品:");
        products.stream()
            .filter(isCheap)
            .map(Product::name)
            .forEach(name -> System.out.println("  " + name));

        BiFunction<Product, Double, Integer> discounted =
            (p, rate) -> (int) (p.price() * (1.0 - rate));
        for (var product : products) {
            System.out.println(product.name()
                + " 10%引き: " + discounted.apply(product, 0.1) + "円");
        }

        UnaryOperator<String> toUpper = String::toUpperCase;
        System.out.println(toUpper.apply("hello")); // HELLO
    }
}

Java 8 / 17 / 21 の完全なサンプルコードは GitHub リポジトリ で確認できます。

Version Coverage

var でラムダ変数の型推論が可能(キャスト構文が必要)。record と組み合わせることで、データの定義と変換パイプラインをコンパクトに書ける。

Java 17
// Java 17: record + Function でデータ変換パイプライン
record Product(String name, int price) {}
var products = List.of(
    new Product("ノートPC", 80000),
    new Product("マウス", 3000)
);
Function<Product, String> toLabel =
    p -> "[" + p.name() + "] \u00a5" + p.price();
products.stream().map(toLabel).forEach(System.out::println);

Library Comparison

標準 API(java.util.function)Function / Consumer / Predicate / Supplier で業務ロジックを部品化するとき。Stream API との親和性が高く、依存ゼロで使える。3引数以上の関数型インターフェースは標準にないため、自作するか BiFunction で部分適用する必要がある。
Vavr(Function0〜Function8)3引数以上の関数やカリー化、部分適用を多用する関数型スタイルのコードを書きたいとき。チーム全体への浸透コストが高く、標準 API と混在するとコードの一貫性が損なわれやすい。
Apache Commons Lang(Failable 系)チェック例外を投げるラムダを Stream 内で扱いたいとき。標準 API では try-catch でラップする必要があるチェック例外を簡潔に書ける反面、依存追加の判断が必要。

注意点

Function<T, T> と UnaryOperator<T> は機能的に同じだが、UnaryOperator を使うと入出力が同じ型であることを型名で明示できる

Consumer は副作用を持つ処理に使うため、Stream の中間操作 peek に渡す場合はデバッグ用途に限定すべき。本番コードで peek に業務ロジックを入れると処理順序の保証が難しくなる

Supplier の遅延評価は get() を呼ぶまで実行されないため、重い初期化処理を Supplier に包むとパフォーマンス改善に使えるが、スレッドセーフではない点に注意

BiFunction<T, U, R> の型パラメータが3つになるため、複雑なラムダ式ではローカル変数に型付きで分離した方が可読性が上がる

FAQ

Function と UnaryOperator はどう使い分けますか。

入出力が同じ型なら UnaryOperator を使うと意図が明確になります。型が異なる変換には Function を使います。機能的な違いはありません。

Consumer を Stream の peek に渡しても大丈夫ですか。

デバッグ目的なら問題ありませんが、業務ロジックを peek に入れると遅延評価で実行されない可能性があります。副作用を伴う処理は forEach で行うのが安全です。

Supplier はどんな場面で使いますか。

重い初期化処理の遅延評価、デフォルト値の生成(Optional.orElseGet)、ファクトリの抽象化などで使います。get() を呼ぶまで処理が実行されない点がポイントです。

関連書籍

この記事のテーマをさらに深く学びたい方へ。

※ Amazon アソシエイトリンクを含みます