概要
Java 8 で導入されたラムダ式と関数型インターフェースは、冗長な匿名クラスを置き換えるだけの構文糖に見えることがあります。しかし実務では、バリデーションルールの合成、価格計算ストラテジーの差し替え、ソート条件の動的組み立てなど、ロジックの部品化と組み合わせに直結する場面で力を発揮します。この記事では、@FunctionalInterface アノテーションの意味と自作インターフェースの定義方法を押さえたうえで、匿名クラスからラムダ式への移行、default メソッドによる合成、Comparator.comparing を使ったソート、メソッド参照の使い分けまでを一通り扱います。Java 17 では record との組み合わせでデータ型が簡潔になり、Java 21 では sealed interface と switch パターンマッチングで Strategy パターンを型安全に表現する選択肢も加わります。外部ライブラリなしで完結するコードを示しながら、現場で迷いやすい判断ポイントを整理します。
使いどころ
入力バリデーションルールを Validator<T> として定義し、and() で複数条件を合成して CSV 取込時に適用する
価格計算の割引ロジック(定額値引き・率引き・会員割引)を関数型インターフェースで差し替え可能にする
一覧画面のソート条件を Comparator.comparing + thenComparing で動的に組み立て、ユーザーの選択に応じて切り替える
コード例
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);
}
}Version Coverage
record でデータ型を簡潔に定義し、Comparator.comparing(Product::price) のようにメソッド参照と組み合わせて可読性が上がる。var による型推論も活用できる。
// Java 17: record + Comparator.comparing + toList()
record Product(String name, int price) {}
var sorted = products.stream()
.sorted(Comparator.comparing(Product::price))
.toList();Library Comparison
注意点
@FunctionalInterface を付けなくてもラムダ式は使えるが、アノテーションを付けておくと抽象メソッドの追加時にコンパイルエラーで検出できる
ラムダ式内で外部の変数を参照する場合、その変数は実質的に final でなければならない。ループ変数を直接キャプチャしようとするとコンパイルエラーになる
Comparator.comparing のメソッド参照で null を含むフィールドを渡すと NullPointerException になる。Comparator.nullsFirst / nullsLast で明示的に順序を指定すること
匿名クラスとラムダ式では this の参照先が異なる。ラムダ式の this は外側のクラスを指すため、自身のインスタンスを参照したい場合は匿名クラスを使う必要がある
FAQ
付けなくても抽象メソッドが1つなら使えます。ただしアノテーションを付けると、メソッド追加時にコンパイルエラーで気付けるため、自作インターフェースには付けておくのが安全です。
単一メソッドの委譲なら String::trim のようにメソッド参照の方が読みやすくなります。引数の加工や複数処理を含む場合はラムダ式を使います。
this の参照先が変わる点と、複数の抽象メソッドを持つインターフェースには使えない点に注意してください。それ以外は基本的にラムダ式へ置き換えて問題ありません。