java-recipes

ホーム オブジェクト指向設計(OOP) › OOP-03

OOP-03: 関数型インターフェース・ラムダ式との関係

Java 8 で導入されたラムダ式は「@FunctionalInterface(抽象メソッドが1つのインターフェース)」を 匿名クラスよりも簡潔に実装する仕組みです。 Strategy パターンをラムダで書き換える例を通じて、関数型インターフェースの本質を理解しましょう。

@FunctionalInterface とは何か

Java のインターフェースのうち、抽象メソッドがちょうど1つだけのものを「関数型インターフェース」と呼びます。@FunctionalInterface アノテーションをつけることで、 コンパイラがその制約(抽象メソッドは1つだけ)を確認してくれます。

ラムダ式が使える理由

ラムダ式は「インターフェースを実装した匿名クラスのインスタンス」の省略形です。 抽象メソッドが1つだけなので、ラムダ式のコード(-> の右辺)が 「その1つのメソッドの実装」として自動的に対応付けられます。

書き方特徴
匿名クラスnew Validator() { @Override ... }Java 7 以前からある書き方。冗長だが明示的
ラムダ式value -> value != nullJava 8 以降。簡潔で読みやすい
メソッド参照System.out::println既存のメソッドをそのまま渡す。さらに簡潔

サンプルコード

FunctionalInterfaceSample.java
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.function.Predicate;

public class FunctionalInterfaceSample {

    // @FunctionalInterface: 抽象メソッドが1つのインターフェース
    @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);
    }

    static class Product {
        final String name;
        final int price;

        Product(String name, int price) {
            this.name = name;
            this.price = price;
        }

        @Override
        public String toString() {
            return name + "(" + price + "円)";
        }
    }

    public static void main(String[] args) {
        System.out.println("=== @FunctionalInterface をラムダで実装 ===");
        // 匿名クラスの従来の書き方
        Validator<String> notEmptyAnon = new Validator<String>() {
            @Override
            public boolean validate(String value) {
                return value != null && !value.isEmpty();
            }
        };

        // ラムダ式(同等だがシンプル)
        Validator<String> notEmpty = value -> value != null && !value.isEmpty();
        Validator<String> notTooLong = value -> value.length() <= 20;
        Validator<String> combined = notEmpty.and(notTooLong);

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

        System.out.println("\n=== Strategy パターンをラムダで ===");
        PriceCalculator noDiscount = price -> price;
        PriceCalculator tenPercent = price -> (int) (price * 0.9);
        PriceCalculator halfPrice = price -> price / 2;

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

        System.out.println("\n=== Comparator(関数型インターフェース)でソート ===");
        List<Product> products = Arrays.asList(
            new Product("A", 3000),
            new Product("B", 1000),
            new Product("C", 2000)
        );

        // 匿名クラス
        Collections.sort(products, new Comparator<Product>() {
            @Override
            public int compare(Product a, Product b) {
                return Integer.compare(a.price, b.price);
            }
        });
        System.out.println("価格昇順: " + products);

        // ラムダで同等
        products.sort((a, b) -> Integer.compare(b.price, a.price)); // 降順
        System.out.println("価格降順: " + products);

        // メソッド参照
        System.out.println("\n=== メソッド参照 ===");
        List<String> names = Arrays.asList("田中", "山田", "鈴木");
        names.forEach(System.out::println); // インスタンスメソッド参照
    }
}

Java 8 で導入されたラムダ式は、@FunctionalInterface(抽象メソッドが1つのインターフェース)を実装するときに匿名クラスの代わりに使えます。Comparator や Runnable など、既存の多くのインターフェースもラムダで実装できます。

よくあるミス・注意点

⚠️ @FunctionalInterface アノテーションは必須ではないが、つけることを推奨

@FunctionalInterface は必須ではありません。 アノテーションなしでも抽象メソッドが1つであればラムダで実装できます。 ただし、アノテーションをつけることで「このインターフェースは関数型として使うことを意図している」という意図が明確になり、 誤って抽象メソッドを2つ追加してしまったときにコンパイルエラーで気づけます。 自分で定義する関数型インターフェースには、積極的にアノテーションをつけましょう。

⚠️ ラムダ式の中で例外をスローするときの注意

ラムダ式の中で検査例外(IOException など)をスローしようとすると、 インターフェースのメソッド宣言に throws IOException がないとコンパイルエラーになります。 例外が発生しうる処理をラムダに渡す場合は、ラムダの中で try-catch して非検査例外(RuntimeException)に変換するか、throws IOException を宣言したカスタム関数型インターフェースを定義してください。

テストする観点

  • 匿名クラスとラムダ式で実装した Validator が同じ引数に対して同じ結果を返すこと(同等性の確認)
  • Validator.and() の合成結果が、各バリデーションを個別に呼び出した結果の AND と一致すること
  • メソッド参照(System.out::println)が対応するラムダ式(s -> System.out.println(s))と同等に動作すること
  • PriceCalculator の各戦略(通常・10%引き・半額)が期待通りの価格を計算すること(境界値: 価格 0 のとき)
  • Comparator.comparing() によるソートが正しい順序でリストを返すこと

GitHub でソースコードを見る →