2016年7月30日土曜日

[JavaSE8 Goldへの道] その2 関数型インタフェース


猫と一緒にガジェットライフ♪ムチャです。

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」ラベルを付けていきます。
よろしければおつきあいくださいませ。

それではみなさまよきガジェットライフを(´∀`)ノ




▼こちらの記事もどうぞ

▼ブログを気に入っていただけたらRSS登録をお願いします!
▼ブログランキング参加中!応援よろしくお願いします。

スポンサーリンク