関数型インターフェース
概要
ざっくり言うと、Javaで「関数を(型として)扱うためのインターフェース」が関数型インターフェースです。
関数を第一級オブジェクト(first-class object)として扱えないJavaで、第一級っぽく扱うための仕組みとしてJava 8で導入されました。そのため、「関数型インターフェース」という言葉は基本的にJavaの用語です1。
定義
具体的には「 抽象メソッドを1つだけ持つインターフェース」と定義され、SAM(Single Abstract Method)インターフェースとも言います。
もう少し細かく言うと、主に下記のルールがあります。
- 抽象メソッドは1つだけ
- 2つ以上の抽象メソッドを定義するとコンパイルエラー
- 非抽象メソッドなら複数あってもいい
- 継承しているインターフェースの抽象メソッドも合算して1つだけ
- プロパティや定数などは持っていい
@FunctionalInterface // 関数型インターフェースの明示的宣言。抽象メソッドが2個以上あるとコンパイルエラーになる
public interface Logger {
void log(String message);
}
Java公式の関数型インターフェース(functional interface
)の説明は↓
Kotlinでの関数型インターフェース
Kotlin 1.4からはfun interface
キーワードで関数型インターフェースを宣言できます。
fun interface Logger {
fun log(message: String)
}
Kotlin 1.3までは、Javaの関数型インターフェースをKotlinから利用することはできましたが、Kotlin側で関数型インターフェースを作成することはできませんでした。
なお、fun interface
はコンパイルすると@FunctionalInterface
付きの通常のインターフェースになるためJavaからも利用でき、JavaとKotlinで相互に運用できます。
何がうれしいの?
ラムダやメソッド参照でサクッと実装できる
関数型インターフェースは関数を扱うためのインターフェースであり、抽象メソッドを1つしか持てないと説明しました。これは言い換えれば、その単一メソッドさえ実装すれば完成するということです。
この単一メソッドの実装として、ラムダ式やメソッド参照を渡すことができます。するとSAM変換2という仕組みにより、匿名クラスのインスタンスが自動で生成されます。
// メソッド参照
val logger = Logger(::println)
このように、実装クラスを明示的に書くことなくラムダ式やメソッド参照で簡潔に実装できるのは関数型インターフェースのメリットの1つです。
またこれにより、テスト用のモックやスタブを用意するのも簡単になります。
名前をつけられるので意図が明確になる
単なる関数型((T) -> R
など)では引数と戻り値の型しか分かりません。これに対し関数型インターフェースでは名前をつけることができます。
例えばConverter<T, R>
であれば、T
をR
に変換する処理なのだという意図が自然に伝わり、可読性が向上します。
fun interface Converter<T, R> {
fun convert(value: T): R
}
Java標準APIとの親和性
関数型インターフェースは、Javaの標準APIでも広く利用されています。
例えば以下のような場面では、関数型インターフェースが前提となっています。
ストリームAPI
map
やfilter
に渡す関数はFunction<T, R>
やPredicate<T>
などの関数型インターフェースです。
users.stream()
.filter { it.age >= 30 } // Predicate<User>
.map { it.name.uppercase() } // Function<User, String>
.toList()
スレッド処理
Runnable
はrun()
メソッド1つだけの関数型インターフェースなので、Thread { ... }
のように書けます。
Thread { fetchRemoteData() }.start()
ソートや比較
Comparator<T>
はcompare(T, T)
メソッド1つだけを持つため、ラムダで簡潔に書けます。
products.sortedWith { a, b -> a.price - b.price } // Comparator<Product>