関数型インターフェースを一言でいうと
関数型インターフェースは、
「たった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
}
Javacalc は、「“具体的な計算内容”は知らないけれど、その計算を表す関数(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 つだけ」であることです。
既存のインターフェースも、条件を満たしていれば関数型になれる
Runnable や Comparator、Callable のような、
Java 8 以前からあるインターフェースも、「抽象メソッドが 1 つだけ」なら関数型インターフェースとして扱えます。
Java 8 では、それらに @FunctionalInterface が追記されています。
つまり、古くからあるインターフェースを
「関数型インターフェース」という新しい視点で捉え直した、とも言えます。
まとめ:関数型インターフェースを自分の言葉で整理する
関数型インターフェースを、あなたの中で一文にするとしたら、
「1 つだけ抽象メソッドを持つインターフェースで、その 1 メソッドを“関数”としてラムダ式で渡せるようにするための型」
です。
押さえておきたいのは、
抽象メソッド 1 つだけ → ラムダ式で実装できる@FunctionalInterface で「関数型であること」を明示&チェックできるRunnable, Comparator, Callable, Supplier, Consumer, Function, Predicate などが代表例
自分でも簡単に定義できて、メソッドに「処理そのもの」を引数で渡せるようになる
というあたりです。
