猫と一緒にガジェットライフ♪ムチャです。
Javaにラムダ式を追加するに当たって新しく用意された概念が「関数型インタフェース」です。
JavaSE8 Goldへの道(Upgrade to Java SE 8 Programmer 1Z0-810 試験対策)2回目は、この関数型インタフェースについてまとめます。
関数型インタフェースとは
関数型インタフェースの定義は「ただ1つの抽象メソッドが定義されたインタフェース」です。
これはJava8になって新しく導入された概念で、例えば前回例に挙げた
Runnable
インタフェースは従来からあるインタフェースですが、runメソッドだけが定義されているので関数型インタフェースです。@FunctionalInterface public interface Runnable { public abstract void run(); }
関数型インタフェースであることを明示するために
@FunctionalInterface
というアノテーションが追加されました。これを付けなくてもコンパイラは定義されている抽象メソッドが1つであれば関数型インタフェースとして扱ってくれますが、付けてあれば関数型インタフェースの条件を満たしてない場合にコンパイルエラーになります。自分で定義したインタフェースでも、抽象メソッドが1つであれば関数型インタフェースになります。メソッド名は何でも構いません。
関数型インタフェースの条件はもう少しあって、全て書き出すと以下になります。
- 単一の抽象メソッドを持つ
- ただしstaticメソッド、デフォルトメソッドは除く
- java.lang.Objectクラスのpublicメソッドは抽象メソッドとしての宣言は可能
staticメソッド、デフォルトメソッドもJava8で新しく追加された機能で、詳細は別の回に書きますが、インタフェースに実装を持てるようになりました。
なので、以下のコードはいろいろ宣言されていますが、条件を満たすので関数型インタフェースです。
Object
クラスのpublicメソッドで該当するのは、equals,toString,hashCode
の3つです。ややこしいですが、cloneはprotectedなので対象外ですし、waitなどはfinalなのでオーバーライドができないため対象外です。なので、以下のコードはいろいろ宣言されていますが、条件を満たすので関数型インタフェースです。
@FunctionalInterface public interface TestInterface { void func(); boolean equals(Object o); default void def(){ System.out.println("デフォルトメソッド"); } static int staticmethod() { return 1; } }
前回も書きましたが、eclipseなどで開発する場合はエラーがすぐに出るのでおぼろげでも問題ありませんが、試験では問われると思いますので覚えておきましょう。
関数型インタフェースとラムダ式
Javaにおけるラムダ式は、この関数型インタフェースの実装(匿名クラスの記述)を簡略化して書けるものと言っても問題は無いのではと思います。実際以下のコードはコンパイルが通りますし、実行結果も同じです。
//匿名クラス new Thread(new Runnable(){ @Override public void run(){ System.out.println("( ´∀`)<ぬるぽ"); } }).start(); //ラムダ式 new Thread( () -> System.out.println("( ´∀`)<ぬるぽ"); ).start();
ラムダ式は関数インターフェースとしての型を持っており、引数の数・型・戻り値の型が合っていれば、変数へ入れられます。
Runnable r = () -> System.out.println("( ´∀`)<ぬるぽ"); new Thread(r).start();
ですので、ラムダ式を「なんかよくわからないもの」としてとらえるのではなく、あくまで従来の匿名クラスと同じなんだと考えると、それほど難しくないのではと思います。
※1 じゃあ実際に匿名クラスとして実行されているのかというとそうでもなく、もう少し効率的に実行できる仕組みを利用しているようです。
具体的には、コンパイラはラムダ式をJava7で追加されたバイトコード命令のinvokedynamicを吐くようにしておいて、実行時にどのように生成するかを実装側で決められるようにしたということです。
地道にラムダ式毎に匿名クラスしてももちろん問題無いわけですが、そうするとガンガン匿名クラスのオブジェクトコードができてクラスロードに時間がかかるのを懸念したそうです。
※2 あくまで従来からあるインタフェースの仕組みでもあるので、匿名じゃなくても普通にクラス定義しても動きます。
あらかじめ用意された関数型インタフェース
Java8で新たにjava.util.functionパッケージが新設され、ラムダ式を使いそうな局面で必要となるような関数型インタフェースが多数用意されています。その数じつに47個!
さすがに全部覚えるのは大変ですが、基本的なものがいくつかあって残りはその派生なので、それらを解説します。
※以下インタフェース定義ではデフォルトメソッド、staticメソッドは省略しています。
Function<T,R>
一番分かりやすいのがFunctionでしょう。引数を1つとって結果を生成する関数です。@FunctionalInterface public interface Function<T,R>{ R apply(T t); } Function<Integer, String> f = (n) -> String.valueOf(n);
Consumer<T>
値を受け取って何も返さない関数です。関数型言語でよく出てくる、「副作用」を伴う操作を期待されます。@FunctionalInterface public interface Consumer<T>{ void accept(T t) } Consumer<String> c = s -> System.out.println(s);
Supplier<T>
引数はなく、呼び出すと値を返す関数です。@FunctionalInterface public interface Supplier<T>{ T get(); } Supplier<String> s = () -> "テスト!";
Predicate<T>
受け取った値を評価してbooleanを返す関数です。@FunctionalInterface public interface Predicate<T>{ boolean test(T t); } Predicate<Integer> p = (n) -> n % 2 == 0;
UnaryOperator<T>
引数と同じ型の結果を返す関数です。Fanctionの引数と戻りが同じ型の物です。実際Functionを継承しています。
@FunctionalInterface public interface UnaryOperator<T> extends Function<T,T>{ //メソッドはFunctionのapplyメソッドを継承 } UnaryOperator<String> u = s -> s + "テスト";
基本はこの5つを覚えておけばよいと思います。
派生として引数が2つになったBi~(
UnaryOperator
に対してはBinaryOperator
)、引数がプリミティブ型のもの(Int~、Double~など)、戻り値がプリミティブ型のもの(ToInt~、ToDouble~など)、その両方(IntToLongFunction
など)があります。Javaの仮型パラメータ(<>の中のもの)にはプリミティブ型は指定できないため、Integerなどのラッパークラスを使う必要があります。
その場合でもオートボクシング・アンボクシングの仕組みがあるので、実際に使うときにプリミティブ型を渡したり返したりできます。
//Math.randomの戻り値の型はdouble Function<Integer, Double> f = n -> Math.random(); System.out.println(f.apply(10));
実行結果
0.2890477848372678
しかし当然ながらプリミティブ型とラッパー型への変換にはコストがかかりますので、予めプリミティブ型を使うことが分かっている場合は、用途に合ったプリミティブ型の関数型インタフェースを使った方が良いです。
ただし、用意されているのは
int,double,long,boolean
のみで、 char,float,byte
に特化した関数型インタフェースはありません。ちなみに
boolean
に特化したインタフェースはBooleanSupplier
のみです。Predicate<T>
は戻り値がboolean
ですが、まあif文などで判定に使うので他とはちょっと違うと言えるでしょう。今回のまとめ
- 抽象メソッドが1つだけ定義されたインタフェースを関数型インタフェースとする
- ただしstaticメソッド、デフォルトメソッド、
Object
のpublicメソッドは数えない @FunctionalInterface
アノテーションを付けると警告がでるので間違えずに済むが、付けなくても条件を満たしていれば関数型インタフェースとして機能する- 型があっていればラムダ式を関数型インタフェース型の変数へ入れられる
- あらかじめいくつかの関数型インタフェースが
java.util.function
に用意されているので、基本の5つとその派生として覚える - プリミティブ型に特化した関数型インタフェースもある
実際に使うときは「ふーんそんなのがあるのね」くらいで大丈夫ですが、試験では正しい記述方法など問われるので覚えておきましょう。
一連の記事は「JavaSE8Gold」ラベルを付けていきます。
よろしければおつきあいくださいませ。
それではみなさまよきガジェットライフを(´∀`)ノ