🔗

【Java】関数型インターフェースの覚え方 Consumer<T> = Function<T, Void>

2022/11/06に公開約3,700字

はじめに

Javaの関数型インターフェースの名前って覚えにくいですよね。Function, Supplier, Consumer, Predicate... しかもプリミティブ型はそれぞれ型がありますし、Biとかもあります。
ということで、私なりの覚え方を文章化してみました。お役に立てば幸いです。

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

java.util.function パッケージ下のインターフェースだったり、@FunctionalInterface アノテーションがついたインターフェースだったりしますが、実はどちらも不正確です。

Java Language Specification 9.8 Functional Interfacesに定義が記されています。

A functional interface is an interface that is not declared sealed and has just one abstract method (aside from the methods of Object), and thus represents a single function contract.
This "single" method may take the form of multiple abstract methods with override-equivalent signatures inherited from superinterfaces; in this case, the inherited methods logically represent a single method.

2文目は細かい条件を定義しているだけなので無視して(詳細は上のリンクに記載されています)、1文目をDeepL先生に訳してもらったのがこちらです。

関数型インターフェースとは、sealed宣言されておらず、(Objectのメソッドは別として)ただ1つのabstractメソッドを持つインターフェースであり、したがって単一の関数契約を表しているものである。

つまり、abstractメソッドが1つだけ含まれているインターフェースです。
java.util.function パッケージは良く使う形の関数型インターフェースを集めたに過ぎず、@FunctionalInterface アノテーションは関数型インターフェースであると示しているに過ぎません。
(@FunctionalInterface をつけていて関数型インターフェースでないとコンパイルエラーになります。@Override みたいな感じですね)

abstractメソッドがただ1つなら良いので、default, static, privateメソッドは含まれていても問題ありません。実際、Functionはdefaultメソッドを、Predicateはdefault, staticメソッドを含んでいます。

ラムダ式を使って表されることが多いですが、実態はこうなので普通に関数型インターフェースを実装したクラスを作っても動きます。

詳細
  • 「sealed宣言されていない」は、実装できる型を制限していないという意味です。(Java 17で追加された機能)
  • 「Objectのメソッドは別にして」は、Objectクラスのメソッド(equals, hashCode, toString, etc...)は含まないという意味ですが、abstractメソッドはそもそもObjectクラス(というか具象クラス)に含まれていないので意味がないような気はします。

以下2記事がシンタックスシュガー(?)についてわかりやすいです。
https://qiita.com/sano1202/items/64593e8e981e8d6439d3
https://qiita.com/sano1202/items/40cc8a0e29def0c76fa8

覚え方

全ての関数型インターフェースは Function<T,R> の派生であると考えます。

例えば、Consumer<T>Function<T,R> の仮型引数Rが Void である場合、という感じです。
こうすると、ラムダ式の (String a) -> {return Integer.valueOf(a)} という表記の左側をT、右側のreturn文をRと考えられます。

java.util.functionパッケージの関数型インターフェースをこの考え方で表すと以下のようになります。

  • Function<T,R>
  • Predicate<T> => Function<T, Boolean>
  • Consumer<T> => Function<T, Void>
  • Supplier<T> => Function<Void, T>
  • UnaryOperator<T> => Function<T, T>
  • IntFunction<R> => Function<Integer, R>
  • ToIntFunction<T> => Function<T, Integer>
  • BiFunction<T, U, R> => Function<Pair<T, U>, R>
    (Pair<A, B> は AとBのフィールドを1つずつ持ったレコードのようなものです)

このように整理するとまだ覚えやすいのではないでしょうか。
他の関数型インターフェースも同様に考えられます。

  • ObjDoubleConsumer<T> => Function<Pair<T, Double>, Void>
  • IntToLongFunction => Function<Integer, Long>
  • BooleanSupplier => Function<Void, Boolean>
  • IntPredicate => Function<Integer, Boolean>
  • Runnable => Function<Void, Void>

なぜプリミティブ型には専用のインターフェースが用意されているか

パフォーマンスのため、らしいです。
List<Integer> には配列(int[])があるとはいえ IntList が用意されていないですから、Stream APIが速度重視なんでしょう、多分。

ちなみに CharFunction<R> とか FloatFunction<R> はそもそもないですし、BooleanSupplier はあっても BooleanConsumer はありません。
使用頻度が低いものはFunction + ラッパークラスを使えばいいのでわざわざ用意されていないということですね。

おわりに

以上、私なりの覚え方というか整理方法(?)でした。
プリミティブ型のとか UnaryOperator とかはパフォーマンスを気にしなければ無理に使わなくてもいいので、とりあえず適当に書いて後から修正するって感じでいいのかなと。
ちなみに、Project Valhallaっていうプリミティブ型を楽に扱えるようにするJavaのプロジェクトがあるので、早く実装されると嬉しいですね。願わくば次のLTS (Java 21)までには…

GitHubで編集を提案

Discussion

ログインするとコメントできます