Java 8から導入されたラムダ式はJavaによるプログラミングに革新的な変化をもたらした。Javaのラムダ式は、入力パラメータを受け取って値を返す無名関数の簡易的な記述法である。入力パラメータは、明示的に型を宣言することもできれば、型宣言を省略することもできる。Java 11から、このラムダ式の入力パラメータ宣言において、Java 10で導入されたvarを使用できるようになった。
ラムダ式の入力パラメータ
ラムダ式の入力パラメータの宣言には、明示的に型を指定する方法と、型を指定しない方法の2種類がある。次のコードは、明示的に型を指定して宣言した例である。
// 引数にStringを1つ受け取り、戻り値としてStringを返す
Function<String, String> f = (String name) -> "Hi, " + name;
System.out.println( f.apply("Taro") ); // "Hi, Taro"
// 引数にIntegerを1つ受け取り、戻り値を返さない
Consumer<Integer> c = (Integer value) -> System.out.println("value=" + value);
c1.accept(123); // "value=123"
// 引数にStringとIntegerを受け取り、戻り値としてStringを返す
BiFunction<String, Integer, String> bf =
(String name, Integer value) -> name + ":" + value;
System.out.println( bf.apply("Taro", 123) ); // "Taro:123"
Functionインタフェースはラムダ式やメソッド参照の代入先として使用するための関数型インタフェースで、java.util.functionパッケージで提供されている。オペレーションはapply()メソッドで宣言されており、Genericsの1つ目の型引数がオペレーションの引数の型、2つ目の型引数が戻り値の型になる。同様に、Consumerインタフェースは引数をひとつ受け取って戻り値を返さないオペレーションを、BiFunctionインタフェースは引数を2つ受け取って戻り値を返すオペレーションを持つ。
上の例と同じ内容で、入力パラメータの型指定を省略した例が次のコードになる。
Function<String, String> f = name -> "Hi, " + name;
Consumer<Integer> c = value -> System.out.println("value=" + value);
BiFunction<String, Integer, String> bf = (name, value) -> name + ":" + value;
このように、ラムダパラメータでは型宣言を省略しても問題なく実行できる。これは、コンパイラが関数型インタフェースの宣言などから推論して、適切な型を特定してくれるからだ。第6回や第8回の記事で取り上げたローカル変数の型推論と似ているが、ラムダ式の場合は"var"を付ける必要はない。
一般に、型を指定する方法を「明示的な型付け」、指定しない方法を「暗黙的な型付け」という呼び方をする。ラムダパラメータでは、次のように明示的な型付けと暗黙的な型付けを混在させることはできない。このコードはコンパイルエラーになる。
// コンパイルエラー
BiFunction<String, Integer, String> bf = (String name, value) -> name + ": " + value;
ラムダパラメータの型宣言にvarを使う
上で暗黙的な型付けには"var"は必要ないと書いたが、実際には、Java 11以降ではラムダパラメータの型宣言にvarを使うこともできるようになった。この変更は「JEP 323: Local-Variable Syntax for Lambda Parameters」として提案されたもので、次のように記述する。
Function<String, String> f = (var name) -> "Hi, " + name;
Consumer<Integer> c = (var value) -> System.out.println("value=" + value);
BiFunction<String, Integer, String> bf = (var name, var value) -> name + ":" + value;
varを使ったラムダパラメータの型宣言は、明示的な型付けの場合と同様に、暗黙的な型付けと混在させることはできない。したがって、次のような宣言はコンパイルエラーとなる。
// コンパイルエラー
BiFunction<String, Integer, String> bf = (var name, value) -> name + ": " + value;
また、次のようにvarによる型宣言と明示的な型付けの宣言を混在させることもできない。
// コンパイルエラー
BiFunction<String, Integer, String> bf = (var name, String value) -> name + ": " + value;
実際には、varを使った記述法は単なるシンタックスシュガーで、プログラム的には暗黙的な型付けとまったく同じになる。Java 10でvarが導入される以前から、ラムダ式においてはすでに型推論が実装されていたため、このような仕様になっている。varが使えるようにしたおもな目的は、varを使ったローカル変数の構文とラムダパラメータの構文を合致させるためである。
ラムダパラメータにアノテーションを使う
単なるシンタックスシュガーとは言え、メリットもある。varを使ってラムダパラメータを定義した場合には、アノテーションを使えるようになることだ。アノテーションは明示的な型宣言の場合にも使えるが、暗黙的な型宣言では使うことができない。varが使えるようになったことで、型推論を有効にしたままでアノテーションを付加することができるようになったわけだ。
次のコードは、ラムダパラメータにNullを許容しないチェックを付加する@NotNullアノテーションを指定した例である。
Function<String, String> f = (@NotNull var name) -> "Hi, " + name;
Consumer<Integer> c = (@NotNull var value) -> System.out.println("value=" + value);
//System.out.println( f.apply(null) ); // IllegalArgumentException
//c.accept(null); // IllegalArgumentException
本稿では、動作確認にJetBrain社が提供している org.jetbrains.annotations.NotNull を使た。@NotNullアノテーションを付けない場合は、パラメータにnullが渡されてもそのまま実行できてしまうが、@NotNullが付いている場合には IllegalArgumentException が発生する。
別の例も見てみよう。次のコードでは、関数インタフェースとしてExample<T>が宣言されている。Example<T>のメソッドは、引数としてList<T>を受け取る。
// 関数インタフェース
@FunctionalInterface
interface Example<T> {
void print(List<T> list);
}
// 明示的な型宣言
Example<String> ex = (@NotNull List<String> list) -> {
list.stream().forEach((String str) -> System.out.println(str));
};
// varを使った型宣言
Example<String> ex = (@NotNull var list) -> {
list.stream().forEach((var str) -> System.out.println(str));
};
2つのラムダ式の宣言は意味はまったく同じだが、varを使った方が型部分の記述量を少なくできる。アノテーションを使いたいので暗黙的な型宣言は使えない。
ラムダパラメータにvarが使えるようになったことは、機能拡張という意味では非常に軽微なことではあるが、コードの統一性という観点では大きな意味がある。パラメータチェックなどにアノテーションを活用しているケースでは特に有効と言えるだろう。