Java | Java 詳細・モダン文法:ラムダ式・関数型 – @FunctionalInterface

Java Java
スポンサーリンク

@FunctionalInterface を一言でいうと

@FunctionalInterface は、

「このインターフェースは“関数型インターフェース”として使うつもりですよ」とコンパイラに宣言するためのアノテーション

です。

関数型インターフェースとは、「抽象メソッドをちょうど 1 つだけ持つインターフェース」のことです。
この条件を満たしたインターフェースは、ラムダ式やメソッド参照でインスタンス化できます。

@FunctionalInterface を付けると、
「抽象メソッドが 2 個以上になったらコンパイルエラーにする」という“安全装置”が働きます。


そもそも関数型インターフェースとは何か

抽象メソッドが 1 つだけのインターフェース

関数型インターフェースの定義はシンプルです。

「抽象メソッドが 1 つだけのインターフェース」。

例えばこうです。

@FunctionalInterface
public interface MyFunc {
    int apply(int x);
}
Java

抽象メソッド apply が 1 つなので、関数型インターフェースです。

このインターフェースは、ラムダ式で直接実装できます。

MyFunc f = x -> x * 2;
int result = f.apply(3);   // 6
Java

ここで x -> x * 2 というラムダ式は、「int を 1 つ受け取って int を返す関数」として MyFunc に代入されています。

ラムダ式と 1 対 1 で対応する「関数の型」

Java は「関数そのもの」を直接型として持っていません。
そこで、「抽象メソッド 1 つだけのインターフェース」を“関数の型”として扱う、という設計を取りました。

Runnable
Callable<V>
Comparator<T>

など、Java 8 以前からあるインターフェースも、「抽象メソッドが 1 つ」であるため、関数型インターフェースとしてラムダ式に使えます。


@FunctionalInterface の役割とメリット

役割 1:そのインターフェースが「関数型」であることの宣言

@FunctionalInterface をインターフェースに付けると、

「このインターフェースは関数型インターフェースとして使うつもりです」

と宣言できます。

@FunctionalInterface
public interface Converter<F, T> {
    T convert(F from);
}
Java

この宣言のおかげで、読む人にも「これはラムダ用の“関数の入れ物”なんだな」とすぐ伝わります。

役割 2:抽象メソッドが 1 つだけであることをコンパイラが保証

一番大事なのはここです。

@FunctionalInterface が付いたインターフェースに、
うっかり抽象メソッドを 2 つ以上書いてしまうと、コンパイルエラーになります。

@FunctionalInterface
public interface BadFunc {
    void m1();
    // void m2();   // これをコメントアウト解除するとコンパイルエラー
}
Java

これによって、

「関数型インターフェースとして設計したつもりが、
誰かが後から抽象メソッドを追加して“関数型ではなくなってしまう”」

という事故を防げます。

なお、@FunctionalInterface を付けていなくても、
抽象メソッドが 1 つしかなければ、そのインターフェースは関数型インターフェースとして扱われます。
ただし、その場合はコンパイラがチェックしてくれないので、明示的に付けておく方が安心です。


具体例で @FunctionalInterface の意味を体感する

例1:自作の関数型インターフェースとラムダ式

シンプルな例からいきます。

@FunctionalInterface
public interface IntPredicate {
    boolean test(int value);
}
Java

このインターフェースは、「int を受け取って boolean を返す関数の型」です。

使う側はこうです。

public class IntPredicateExample {
    public static void main(String[] args) {
        IntPredicate isEven = x -> x % 2 == 0;
        IntPredicate isPositive = x -> x > 0;

        System.out.println(isEven.test(4));      // true
        System.out.println(isPositive.test(-1)); // false
    }
}
Java

ここで isEvenisPositive も、

int を受け取って boolean を返す処理」

としてラムダ式で定義され、IntPredicate 型に代入されています。

@FunctionalInterface を付けておくことで、

「このインターフェースは“1 メソッドだけをラムダで実装させたい型”なんだな」

という意図が明確になります。

例2:誤って抽象メソッドを増やしたとき

上の IntPredicate に、間違って別の抽象メソッドを追加してみます。

@FunctionalInterface
public interface IntPredicate {
    boolean test(int value);
    // boolean other(int v);  // 追加するとコンパイルエラー
}
Java

other を追加すると、コンパイラが「@FunctionalInterface なのに抽象メソッド 2 つあるよ」と怒ってくれます。

もしアノテーションを付けていなかったら、
「抽象メソッド 2 つのインターフェース」を作れてしまい、
それを関数型インターフェースだと勘違いしてラムダ式で使おうとしてハマる…ということが起きかねません。


@FunctionalInterface とデフォルトメソッド/static メソッド

抽象メソッド以外はいくつあってもよい

関数型インターフェースの条件は、

「抽象メソッドが 1 つだけ」

です。

逆に言うと、

デフォルトメソッド
static メソッド
Object から継承したメソッドのオーバーライド

これらはいくつあっても構いません。

例えば、次は正しい関数型インターフェースです。

@FunctionalInterface
public interface MyFunc {
    int apply(int x);

    default void log(int x) {
        System.out.println("x = " + x);
    }

    static void hello() {
        System.out.println("Hello");
    }
}
Java

apply だけが抽象メソッドで、
log はデフォルト実装付き、hello は static なので、
「抽象メソッドは 1 つ」という条件を満たしています。

Object のメソッド(toString, equals, hashCode など)をオーバーライドしている場合も、それらは「抽象メソッドのカウントから除外される」と仕様で決まっています。


標準 API における @FunctionalInterface の利用

Java 標準の関数型インターフェースたち

java.util.function パッケージには、多数の関数型インターフェースが用意されています。

例えばこういうものです。

Supplier<T>
Consumer<T>
Function<T, R>
Predicate<T>

これらの多くには @FunctionalInterface が付いています。

また、RunnableComparator<T> など、古くからあるインターフェースにも
Java 8 以降で @FunctionalInterface が付けられました。

このアノテーションのおかげで、

「このインターフェースはラムダ式で使うためのものだ」
「抽象メソッドは 1 つだけであると保証されている」

という情報が、コードレベルで明示されています。


@FunctionalInterface をいつ付けるべきか

自分で「ラムダ用のインターフェース」を定義するときは必ず付ける

判断基準はシンプルです。

「このインターフェースを、“関数として渡すための型”として使うか?」

もし答えが「はい」なら、@FunctionalInterface を付けるのがおすすめです。

Callback
Validator<T>
Handler<T>
Converter<F, T>

のような、「何か 1 つの処理を表す」インターフェースを自作するときは、
それが関数型であることを明示した方が、後から読む人にも、自分自身にも親切です。

逆に、通常の“オブジェクトのインターフェース”として設計しているなら、
無理に関数型にする必要はありません。


まとめ:@FunctionalInterface を自分の言葉で整理する

@FunctionalInterface を初心者向けにまとめると、

「このインターフェースは“抽象メソッド 1 つだけ”の関数型インターフェースですよ、とコンパイラと人間に約束する印」

です。

それによって、

ラムダ式やメソッド参照でインスタンス化できる「関数の型」になる
抽象メソッドを 2 つ以上宣言するとコンパイルエラーになり、誤りにすぐ気付ける
デフォルトメソッドや static メソッドはいくつあってもよい

というメリットが得られます。

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