関数型インタフェース

ラムダ式はどのような場面で使用できるのだろうか?

Java 8では実装するべきメソッドが1つしかないインタフェースを「関数型インタフェース」として扱うことができ、ラムダ式はこの関数型インタフェースの代わりに使用することができる。前述のサンプルで例として挙げたjava.util.Comparatorも関数型インタフェースとして扱うことができるインタフェースの1つというわけだ。

なお、Java 8ではjava.util.functionパッケージで以下のような汎用的な関数型インタフェースがいくつか用意されており、後述するコレクションAPIなどではこれらのインタフェースが使用されている。ラムダ式を使用可能なメソッドを自作する場合は基本的にこれらの汎用的な関数型インタフェースを使用するといいだろう。

  • Function<T, R> … 1つの引数を受け取って結果を返す関数
  • BiFunction<T, U, R> … 2つの引数を受け取って結果を返す関数
  • UnaryOperator<T> … 単項演算子
  • BinaryOperator<T> … 二項演算子
  • Predicate<T> … 1つの引数を受け取る条件式
  • BiPredicate<T, U> … 2つの引数を受け取る条件式
  • Supplier<T> … 引数を取らず、値を返すだけの関数
  • Consumer<T> … 1つの引数を受け取る戻り値のない関数
  • BiConsumer<T, U> … 2つの引数を受け取る戻り値のない関数

もちろん自分で関数型インタフェースを定義することもできる。その場合は@FunctionalInterfaceアノテーションを付与しておくと、インタフェースが関数型インタフェースの条件を満たしていない場合にコンパイルエラーにすることができる。

@FunctionalInterface
public interface TriFunction {
  public int apply(int a, int b, int c);
}

このようにラムダ式は実際には関数型インタフェースのインスタンスを生成しているにすぎない。そのため直接引数に記述するだけでなく、関数型インタフェース型の変数に代入して再利用したり、メソッドの戻り値としてラムダ式を返したりすることも可能だ。

// ラムダ式を変数に代入
TriFunction function = (a, b, c) -> a + b + c;
int result = function.apply(1, 2, 3); // => 6

// 文字列を指定した回数繰り返す関数を返す
public static Function<Integer, String> repeat(String value){
  return (times) -> {
    StringBuilder sb = new StringBuilder();
    for(int i = 0; i < times; i++){
      sb.append(value);
    }
    return sb.toString();
  };
}
// タブ(\t)を10回繰り返した文字列を取得
String value = repeat("\t").apply(10);

Java SE内でのラムダの活用例としては後述するStream APIが代表的だが、細かい部分ではjava.util.loggingパッケージで提供されているロギングAPIが挙げられる。ロギングAPIは実行時のログレベルによって出力するログをフィルタリングすることが可能だが、ログを出力しない場合は出力文字列を組み立てる処理が無駄な負荷となってしまうため、以下のようにログの前に該当のログレベルが出力対象かどうかのチェック処理を行うことが一般的だった。

if (logger.isLoggable(Level.INFO)) {
  logger.info("Info:" + infoMsg);
}

しかし、Java 8ではLoggerにラムダ式を受け取るメソッドが追加されている。これらのメソッドは実際にログを出力する場合のみラムダ式を評価し、その戻り値をログに出力する。ログを出力しない場合はラムダ式が評価されないため、余計な負荷を最低限に抑えることができるというわけだ。

logger.info(() -> "Info:" + infoMsg);