Java | Java 詳細・モダン文法:ラムダ式・関数型 – 関数型インターフェース

Java Java
スポンサーリンク

関数型インターフェースを一言でいうと

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

「たった1つだけ“抽象メソッド”を持つインターフェースで、その1つのメソッドを“関数”として扱うための入れ物」

です。

ラムダ式は「単体では宙ぶらりん」で、
必ずこの「関数型インターフェース」という“型”の入れ物に入って、はじめて意味を持ちます。

Runnable r = () -> System.out.println("Hello");
Java

ここで Runnable が「関数型インターフェース」です。
run() という抽象メソッドが 1 つだけなので、
()->System.out.println("Hello") という“関数”をぴったり入れられるわけです。


関数型インターフェースの条件と @FunctionalInterface

「抽象メソッドが1つだけ」というルール

関数型インターフェースの条件は、実はとてもシンプルです。

抽象メソッド(abstract なメソッド)が 1 つだけであること。

これだけです。

例えば、次は立派な関数型インターフェースです。

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

抽象メソッド apply(int x) が 1 つだけですよね。
このインターフェースに対しては、ラムダ式がそのまま代入できます。

MyFunction f = x -> x * 2;
int result = f.apply(10);  // 20
Java

ここで大事なのは、「ラムダ式の形」ではなく、「受け取る側の型が“関数型インターフェースかどうか”」です。

MyFunction が関数型インターフェースだからこそ、
x -> x * 2 という“処理そのもの”を代入できるわけです。

@FunctionalInterface は“保証と警告”のためのアノテーション

@FunctionalInterface を付けると、そのインターフェースが関数型であることをコンパイラに宣言できます。

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

これを付けると、

抽象メソッドが 2 つ以上になった瞬間、コンパイルエラーになる
つまり「関数型インターフェースのつもりが、うっかり条件を破る」ことを防げる

というメリットがあります。

なお、@FunctionalInterface を付けなくても、「抽象メソッドが 1 つだけなら」関数型インターフェースとして扱えます。
でも、意図を明示してバグを防ぐために、よく付けます。


代表的な関数型インターフェース(java.util.function)

「この4つ」をまず押さえる

Java 8 以降、java.util.function パッケージに、
よく使う関数型インターフェースが標準で用意されています。

特に登場頻度の高いのはこのあたりです。

Supplier<T>(供給する)

「何も受け取らず、T を 1 つ返す」関数です。

Supplier<String> s = () -> "Hello";
String value = s.get();
Java

関数の形で書くと「() -> T」です。

Consumer<T>(消費する)

「T を 1 つ受け取って、何かする(戻り値なし)」関数です。

Consumer<String> c = s -> System.out.println(s);
c.accept("Hello");
Java

関数の形で書くと「T -> void」です。
forEach などでよく出てきます。

Function<T, R>(変換する)

「T を 1 つ受け取って、R を返す」関数です。

Function<Integer, String> f = x -> "値は " + x;
String s = f.apply(10);
Java

T -> R」のイメージです。
「変換」「マッピング」の場面で多用されます。

Predicate<T>(条件判定する)

「T を 1 つ受け取って、boolean を返す」関数です。

Predicate<String> p = s -> s.length() > 3;
boolean result = p.test("hello");  // true
Java

T -> boolean」というイメージで、「条件」「フィルタ」の場面で使われます。

このあたりを押さえておくと、Stream API やラムダ式の読み書きがかなり楽になります。


自作の関数型インターフェースを定義する

一番シンプルな例:二項演算

例えば、「2 つの int を受け取って int を返す演算」を表す関数型インターフェースを作ってみます。

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

使う側は、ラムダ式で「具体的な演算」を渡せます。

public class IntBinaryOpExample {
    public static void main(String[] args) {
        IntBinaryOp add = (a, b) -> a + b;
        IntBinaryOp mul = (a, b) -> a * b;

        System.out.println(add.apply(3, 5)); // 8
        System.out.println(mul.apply(3, 5)); // 15
    }
}
Java

ここで起きていることを整理すると、

IntBinaryOp という「2 引数 int → int の関数の“型”」を定義した
実装として、(a, b) -> a + b(a, b) -> a * b のようなラムダ式を代入している

という構造です。

メソッドに「関数としての引数」を渡す

これを使うと、「処理を引数として渡す」メソッドが書けるようになります。

public static int calc(IntBinaryOp op, int x, int y) {
    return op.apply(x, y);
}

public static void main(String[] args) {
    int result1 = calc((a, b) -> a + b, 3, 5);
    int result2 = calc((a, b) -> a * b, 3, 5);
    System.out.println(result1); // 8
    System.out.println(result2); // 15
}
Java

calc は、「“具体的な計算内容”は知らないけれど、その計算を表す関数(IntBinaryOp)だけ受け取って実行する」メソッドです。

こうなると、メソッドが「処理の枠組み(アルゴリズム)」だけを担当し、
具体的な“中身”は関数として外から差し込む、というスタイルが取れるようになります。

これが、関数型インターフェース+ラムダ式のすごく大きな価値です。


関数型インターフェースとラムダ式の対応の見抜き方

「抽象メソッドのシグネチャ」と「ラムダの形」を対応させる

関数型インターフェースを読むときに、一番意識したいのがここです。

例えば、Function<T, R> の定義(ざっくり)はこうです。

@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
}
Java

ここだけ見て、「ラムダ式はこういう形になる」とイメージできるようにします。

引数 T を 1 つ受け取って、R を返す。

つまりラムダの形はこうです。

(T t) -> { ... return R; }
Java

実際に使うときは型推論で省略されるので、例えばこんな感じになります。

Function<Integer, String> f = x -> "値は " + x;
Java

同じように、Predicate<T> の定義を見ると、

@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);
}
Java

だからラムダは「T を 1 つ受け取って boolean を返す」。

Predicate<String> p = s -> s.length() > 3;
Java

この「インターフェースのシグネチャ」と「ラムダの形」を対応させて読む癖をつけておくと、
初めて見る関数型インターフェースにもすぐ慣れていきます。


なぜ「関数型インターフェース」という仕組みを経由するのか

Java は「関数単体」を型として持っていない

Java は、もともと「関数そのもの」を型として持っていませんでした。
(C# の delegate や Kotlin の関数型などに比べると、後から補強された形です)

そこで Java 8 では、

「1 メソッドだけ抽象メソッドを持つインターフェース」を
「実質的に“関数の型”として扱う」

という設計を取ったのです。

関数型インターフェースは、「関数の型をインターフェースで表現したもの」と言えます。

ラムダ式は、その「関数型インターフェース」の実装を簡単に書くための記法です。

無名クラスで書いていたものを、ぐっと圧縮した形だと考えると自然です。


関数型インターフェースの制約と注意点

抽象メソッドが2つ以上あると関数型ではなくなる

次のようなインターフェースは、「関数型インターフェースではない」ので、ラムダ式を代入できません。

public interface Bad {
    void m1();
    void m2();   // 抽象メソッドが2つ
}
Java

@FunctionalInterface を付けておけば、
こういう状態になったときにコンパイルエラーで教えてくれます。

@FunctionalInterface
public interface Bad {
    void m1();
    void m2();   // ここでコンパイルエラー
}
Java

一方で、「デフォルトメソッド」や「static メソッド」は、いくつあっても構いません。

@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

大事なのは「抽象メソッドが 1 つだけ」であることです。

既存のインターフェースも、条件を満たしていれば関数型になれる

RunnableComparatorCallable のような、
Java 8 以前からあるインターフェースも、「抽象メソッドが 1 つだけ」なら関数型インターフェースとして扱えます。

Java 8 では、それらに @FunctionalInterface が追記されています。

つまり、古くからあるインターフェースを
「関数型インターフェース」という新しい視点で捉え直した、とも言えます。


まとめ:関数型インターフェースを自分の言葉で整理する

関数型インターフェースを、あなたの中で一文にするとしたら、

「1 つだけ抽象メソッドを持つインターフェースで、その 1 メソッドを“関数”としてラムダ式で渡せるようにするための型」

です。

押さえておきたいのは、

抽象メソッド 1 つだけ → ラムダ式で実装できる
@FunctionalInterface で「関数型であること」を明示&チェックできる
Runnable, Comparator, Callable, Supplier, Consumer, Function, Predicate などが代表例
自分でも簡単に定義できて、メソッドに「処理そのもの」を引数で渡せるようになる

というあたりです。

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