概要

Java 8 で導入されたラムダ式と関数型インターフェースは、冗長な匿名クラスを置き換えるだけの構文糖に見えることがあります。しかし実務では、バリデーションルールの合成、価格計算ストラテジーの差し替え、ソート条件の動的組み立てなど、ロジックの部品化と組み合わせに直結する場面で力を発揮します。この記事では、@FunctionalInterface アノテーションの意味と自作インターフェースの定義方法を押さえたうえで、匿名クラスからラムダ式への移行、default メソッドによる合成、Comparator.comparing を使ったソート、メソッド参照の使い分けまでを一通り扱います。Java 17 では record との組み合わせでデータ型が簡潔になり、Java 21 では sealed interface と switch パターンマッチングで Strategy パターンを型安全に表現する選択肢も加わります。外部ライブラリなしで完結するコードを示しながら、現場で迷いやすい判断ポイントを整理します。

使いどころ

入力バリデーションルールを Validator<T> として定義し、and() で複数条件を合成して CSV 取込時に適用する

価格計算の割引ロジック(定額値引き・率引き・会員割引)を関数型インターフェースで差し替え可能にする

一覧画面のソート条件を Comparator.comparing + thenComparing で動的に組み立て、ユーザーの選択に応じて切り替える

コード例

FunctionalInterfaceDemo.java
import java.util.Comparator;
import java.util.List;
import java.util.function.Predicate;

public class FunctionalInterfaceDemo {

    // 自作の関数型インターフェース
    @FunctionalInterface
    interface Validator<T> {
        boolean validate(T value);

        // default メソッドで合成を表現
        default Validator<T> and(Validator<T> other) {
            return value -> this.validate(value) && other.validate(value);
        }
    }

    // Strategy パターンをラムダで差し替え
    @FunctionalInterface
    interface PriceCalculator {
        int calculate(int basePrice);
    }

    record Product(String name, int price) {}

    public static void main(String[] args) {

        Validator<String> notEmpty = v -> v != null && !v.isEmpty();
        Validator<String> notTooLong = v -> v.length() <= 20;
        var combined = notEmpty.and(notTooLong);

        System.out.println("空文字: " + combined.validate(""));       // false
        System.out.println("OK: " + combined.validate("田中太郎"));    // true

        PriceCalculator tenPercent = price -> (int) (price * 0.9);
        PriceCalculator halfPrice = price -> price / 2;

        int base = 10000;
        System.out.println("10%引き: " + tenPercent.calculate(base)); // 9000
        System.out.println("半額: " + halfPrice.calculate(base));     // 5000

        var products = List.of(
            new Product("A", 3000),
            new Product("B", 1000),
            new Product("C", 2000)
        );
        var byPrice = Comparator.comparing(Product::price);
        var sorted = products.stream()
            .sorted(byPrice)
            .toList();
        System.out.println("価格昇順: " + sorted);

        Predicate<Product> isExpensive = p -> p.price() >= 2000;
        products.stream()
            .filter(isExpensive)
            .forEach(p -> System.out.println("2000円以上: " + p.name()));

        var names = List.of("田中", "山田", "鈴木");
        names.forEach(System.out::println);
    }
}

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

Version Coverage

record でデータ型を簡潔に定義し、Comparator.comparing(Product::price) のようにメソッド参照と組み合わせて可読性が上がる。var による型推論も活用できる。

Java 17
// Java 17: record + Comparator.comparing + toList()
record Product(String name, int price) {}
var sorted = products.stream()
    .sorted(Comparator.comparing(Product::price))
    .toList();

Library Comparison

標準 API(@FunctionalInterface + ラムダ式)自作のバリデーションや計算ロジックを部品化し、default メソッドで合成したいとき。依存ゼロで十分な表現力がある。複雑な合成パイプラインを組むと、型推論のエラーメッセージが読みにくくなることがある。
Vavr (旧 Javaslang)Function0〜Function8 やカリー化、パターンマッチなど関数型プログラミングの語彙をフルに使いたいとき。学習コストが高く、チーム全体に浸透させるのが難しい。標準 API だけで足りる場面で導入すると過剰設計になりやすい。
Google Guava(Function / Predicate)Java 8 以前のプロジェクトで関数型のインターフェースを使いたいとき。Java 8 以降は標準 API の java.util.function と役割が重複するため、新規プロジェクトでは標準 API を優先すべき。

注意点

@FunctionalInterface を付けなくてもラムダ式は使えるが、アノテーションを付けておくと抽象メソッドの追加時にコンパイルエラーで検出できる

ラムダ式内で外部の変数を参照する場合、その変数は実質的に final でなければならない。ループ変数を直接キャプチャしようとするとコンパイルエラーになる

Comparator.comparing のメソッド参照で null を含むフィールドを渡すと NullPointerException になる。Comparator.nullsFirst / nullsLast で明示的に順序を指定すること

匿名クラスとラムダ式では this の参照先が異なる。ラムダ式の this は外側のクラスを指すため、自身のインスタンスを参照したい場合は匿名クラスを使う必要がある

FAQ

@FunctionalInterface を付けないとラムダ式は使えませんか。

付けなくても抽象メソッドが1つなら使えます。ただしアノテーションを付けると、メソッド追加時にコンパイルエラーで気付けるため、自作インターフェースには付けておくのが安全です。

ラムダ式とメソッド参照はどう使い分けますか。

単一メソッドの委譲なら String::trim のようにメソッド参照の方が読みやすくなります。引数の加工や複数処理を含む場合はラムダ式を使います。

匿名クラスをすべてラムダ式に置き換えて問題ありませんか。

this の参照先が変わる点と、複数の抽象メソッドを持つインターフェースには使えない点に注意してください。それ以外は基本的にラムダ式へ置き換えて問題ありません。

関連書籍

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

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