CodeGym /コース /JAVA 25 SELF /関数型インターフェース: @FunctionalInterface

関数型インターフェース: @FunctionalInterface

JAVA 25 SELF
レベル 20, レッスン 3
使用可能

1. はじめに

関数型インターフェースとは、実装を持たない抽象メソッドをちょうど 1 つだけ含むインターフェースのことです。こうしたインターフェースはメソッド実装を簡潔に記述するために使え、Java ではラムダ式(これは後で学びます)を用いて実現します。

なぜメソッドは 1 つだけ?

関数型インターフェースはちょうど 1 つの操作を表すからです。もしメソッドが 2 つ以上あると、どのメソッドを実装すべきかが曖昧になります。したがってルールは簡単です: 1 つのインターフェース = 1 つの抽象メソッド。

標準ライブラリの例

  • Runnable — スレッド内のタスク用(void run())
  • Callable<V> — 結果を返すタスク用(V call())
  • Comparator<T> — オブジェクトの比較用(int compare(T o1, T o2))
  • Consumer<T> — 値の「コンシューマ」(void accept(T t))
  • Supplier<T> — 値の「サプライヤ」(T get())
  • Function<T, R> — T から R への関数(R apply(T t))
  • Predicate<T> — 条件判定(boolean test(T t))

たとえば、インターフェース Runnable は次のとおりです:

public interface Runnable {
    void run();
}

こちらはインターフェース Comparator の例です:

public interface Comparator<T> {
    int compare(T o1, T o2);
    // ... default メソッドや static メソッドは他にもあるが、抽象メソッドは 1 つだけ!
}

重要なポイント: default メソッドと static メソッドは抽象メソッドとは見なされないため、いくつあっても構いません。

2. アノテーション @FunctionalInterface

Java は厳格な言語です。混乱を避けるため、アノテーション @FunctionalInterface を使ってインターフェースを関数型として明示的にマークできます。これは「ボタンは 1 つだけで動きます!」という注意ラベルのようなものです—余計なものを足されないようにするためです。

@FunctionalInterface
public interface Operation {
    int apply(int a, int b);
}

この状態で、もしうっかり 2 つ目の抽象メソッドを追加すると、コンパイラはただちにエラーを報告します:

@FunctionalInterface
public interface Oops {
    void doIt();
    void doSomethingElse(); // エラー!抽象メソッドが 2 つある
}

アノテーションは必須?

いいえ、必須ではありません。抽象メソッドがちょうど 1 つなら、アノテーションがなくてもそのインターフェースは関数型です。ただしアノテーションを付ければ意図を明確に示せるうえ、偶発的なミスから身を守れます。

default や static メソッドを追加してもいい?

はい、可能です!大事なのは、抽象メソッドが 1 つだけであること。他のメソッドは default や static として、好きなだけ追加できます。

例:

@FunctionalInterface
public interface FancyOperation {
    int apply(int a, int b);

    default void printInfo() {
        System.out.println("私はファンシーな操作です!");
    }

    static void description() {
        System.out.println("算術のための関数型インターフェース。");
    }
}

3. 宣言と使用例

たとえば 2 つの数に対する操作を表したいとします。次のように書けます:

@FunctionalInterface
public interface Operation {
    int apply(int a, int b);
}

このインターフェースは様々な方法で実装できます。

通常のクラスで実装する場合

public class SumOperation implements Operation {
    @Override
    public int apply(int a, int b) {
        return a + b;
    }
}

使用例:

Operation sum = new SumOperation();
System.out.println(sum.apply(2, 3)); // 5

匿名クラスで実装する場合

Operation multiply = new Operation() {
    @Override
    public int apply(int a, int b) {
        return a * b;
    }
};
System.out.println(multiply.apply(2, 3)); // 6

ラムダに関する注意

Java 8 以降、この種のインターフェースはラムダ式で手軽に実装できます(より短い構文)。ラムダは数回先の講義で学ぶので、今は「関数型インターフェースはラムダと相性がよく、使い勝手を高めるために用意されている」と理解しておけば十分です。

4. 実践: 自分の関数型インターフェースを書いてみよう

課題 1. 自作の Action を作ろう!

文字列を受け取り、何も返さないインターフェース Action を作成しなさい。文字列を大文字にして印字する匿名クラスで実装しなさい。

@FunctionalInterface
interface Action {
    void act(String s);
}

public class ActionDemo {
    public static void main(String[] args) {
        Action shout = new Action() {
            @Override
            public void act(String text) {
                System.out.println(text.toUpperCase());
            }
        };

        shout.act("java を勉強中!"); // JAVA を勉強中!
    }
}

(後で、ラムダ式を使ってもっと短く書ける方法を見ていきます。)

課題 2. 数値フィルタ

NumberPredicate インターフェースを作り、メソッド boolean test(int n) を定義しなさい。匿名クラスで偶数判定を実装しなさい。

@FunctionalInterface
interface NumberPredicate {
    boolean test(int n);
}

public class PredicateDemo {
    public static void main(String[] args) {
        NumberPredicate isEven = new NumberPredicate() {
            @Override
            public boolean test(int n) {
                return n % 2 == 0;
            }
        };

        System.out.println(isEven.test(4)); // true
        System.out.println(isEven.test(7)); // false
    }
}

課題 3. 標準インターフェースを使う

自作インターフェースの代わりに、用意済みの Predicate<Integer> を使っても構いません:

import java.util.function.Predicate;

Predicate<Integer> isPositive = new Predicate<Integer>() {
    @Override
    public boolean test(Integer x) {
        return x > 0;
    }
};

System.out.println(isPositive.test(10)); // true
System.out.println(isPositive.test(-5)); // false

表: 標準ライブラリの関数型インターフェース

インターフェース メソッド 説明 使用例
Runnable
void run()
引数も戻り値もないタスク スレッド、タイマー
Callable<V>
V call()
結果を返すタスク スレッド、ExecutorService
Comparator<T>
int compare(T, T)
2 つのオブジェクトの比較 コレクションのソート
Consumer<T>
void accept(T)
値の「コンシューマ」 コレクションの反復処理
Supplier<T>
T get()
値の「サプライヤ」 遅延初期化、データ生成
Function<T, R>
R apply(T)
T から R への関数 データ変換
Predicate<T>
boolean test(T)
条件判定 コレクションのフィルタリング

5. 関数型インターフェースでよくあるミス

エラー1: 2 つ目の抽象メソッドを追加してしまった。 インターフェースに抽象メソッドが 2 つ以上あると、もはや関数型ではありません。コンパイラ(特に @FunctionalInterface が付いている場合)はすぐにエラーを報告します。

エラー2: default や static メソッドは抽象メソッドに数えないことを忘れている。 それらは関数型インターフェースに自由に追加できます—「抽象メソッドは 1 つだけ」というルールには反しません。

エラー3: メソッドシグネチャを誤って実装した。 たとえばインターフェースは 2 つの引数を要求しているのに、1 つだけのメソッドを書いてしまう、など。常にシグネチャを確認しましょう。

エラー4: @FunctionalInterface を付けず、うっかりインターフェースを台無しにしてしまう。 アノテーションを付けないと、余計なメソッドを追加してしまい、なぜコードが動かないのか長時間悩むことになり得ます。意図を明確にするためにも、基本的にはアノテーションを付けるのがおすすめです。

1
タスク
JAVA 25 SELF, レベル 20, レッスン 3
ロック未解除
あなた専用の通知システム
あなた専用の通知システム
1
タスク
JAVA 25 SELF, レベル 20, レッスン 3
ロック未解除
ゲームチェッカー:ハイスコア
ゲームチェッカー:ハイスコア
1
タスク
JAVA 25 SELF, レベル 20, レッスン 3
ロック未解除
テキストバリデータ: 文字列長の検証
テキストバリデータ: 文字列長の検証
1
タスク
JAVA 25 SELF, レベル 20, レッスン 3
ロック未解除
便利な出力を備えたテキストコンバータ
便利な出力を備えたテキストコンバータ
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION