Java 10で導入された型推論
通常、Javaのプログラムで変数を宣言する際は、明示的にデータ型を示す必要がある。しかし、Java 10からは、一定の条件を満たしていれば型の指定を省略できるようになった。これはJEP 286として導入された機能である。すでにJShellの解説の際にその一例をお見せしているが、今回はもう少し具体的にこの型推論の使い方について解説する。
Javaの型推論は、変数の宣言時に導入された「var」というキーワードを型の代わりに記述することによって、コンパイル時に適切な型として認識してくれるというものである。例えば、次のコードは通常通り型付きで変数を宣言したものだ。
int num = 123;
String str = new String("Hello");
これは、Java 10以降では次のように「var」をデータ型の代わりに使うことができる。この場合、numはint型として、strはString型として扱われる。
var num = 123;
var str = new String("Hello");
このvarによる型推論はローカル変数でのみ利用できる。クラスに宣言されるインスタンス変数やstaticなクラス変数に対しては、型推論を使うことはできないので注意してほしい。
もう1つ注意しなければならないのは、varによる型推論はあくまでもコンパイラが自動で適切な型を判別してコンパイルしてくれる機能であって、Javaから型そのものがなくなったわけではないという点だ。コンパイルの結果として生成されるバイトコード内の変数は、Java 9以前と同様にすべて型付きの変数になる。したがって、コンパイルの時点で型が明確に推論できないような場合はvarを使うことはできない。
さまざまな型に対する型推論の例
それでは、実際に型推論の使用例をいくつか見ていこう。今回のようなちょっとしたコードの挙動を試すのであれば、JShellを使うと便利である。
まず、基本型に対してvarを使用してみる。
jshell> var num = 123;
num ==> 123
jshell> num = 123.4;
| エラー:
| 不適合な型: 精度が失われる可能性があるdoubleからintへの変換
| num = 123.4;
| ^---^
jshell> var flag = true;
flag ==> true
jshell> flag = false;
flag ==> false
jshell> flag = 123;
| エラー:
| 不適合な型: intをbooleanに変換できません:
| flag = 123;
| ^-^
上の1つ目の例は、右辺が整数リテラルであるため、numはint型として推論される。したがって、numに小数の値を代入しようとするとエラーになる。flagは変数宣言の右辺がtrueなので、bool型として推論される。したがって、整数を代入することはできない。
次の例のnum2は、int型の範囲に収まらない巨大な整数で初期化しようとしている。この場合でも、num2はint型として推論されるため、初期化できずにエラーとなる。整数リテラルの値はデフォルトでint型とみなされ、自動でlong型として推論されることはないので注意が必要だ。
jshell> var num2 = 123456789012345;
| エラー:
| 整数が大きすぎます。
| var num2 = 123456789012345;
| ^
long型として扱ってほしい場合は、次のように'L'や'l'(小文字のエル)を末尾に付ける必要がある。
jshell> var num2 = 123456789012345L;
num2 ==> 123456789012345
配列に対してvarを使用することもできるが、次のような初期化をしようとするとエラーになってしまう。
jshell> var array = {1, 2, 3, 4, 5};
| エラー:
| ローカル変数arrayの型を推論できません
| (配列初期化子には明示的なターゲット型が必要です)
| var array = {1, 2, 3, 4, 5};
| ^--------------------------^
これはエラーメッセージにも書かれているように、配列の型が明示的に指定されておらず、コンパイラが1つの型に特定することができないためだ。配列の宣言にvarを使う場合は、次のように明示的に型を指定して初期化する必要がある。
jshell> var array = new int[]{1, 2, 3, 4, 5};
array ==> int[5] { 1, 2, 3, 4, 5 }
次の例では、Stringクラスのインスタンスのための変数をvarで宣言している。strはString型に推論されるため、通常のStringインスタンスと同様にメソッド呼び出しができる。
jshell> var str = new String("Hello");
str ==> "Hello"
jshell> str.length();
$6 ==> 5
ジェネリクスを使用している場合もvarを使うことができる。
jshell> var list = new ArrayList<String>();
list ==> []
jshell> list.add("apple");
$9 ==> true
jshell> list.add("banana");
$10 ==> true
jshell> list.get(1);
$11 ==> "banana"
jshell> list.add(123);
| エラー:
| 不適合な型: intをjava.lang.Stringに変換できません:
| list.add(123);
| ^-^
この例では、listはArrayList<String>に推論される。この場合は型引数にStringが指定されているので、String以外の値を追加しようとすれば当然エラーになる。
次のコードは、メソッドの戻り値をvar宣言の変数で受け取る例だ。getPath()メソッドやnewBufferedReader()メソッドの戻り値は、それぞれPath型およびBufferedReader型だということが明らかなので、varによる型推論を使うことができる。
jshell> var path = FileSystems.getDefault().getPath("hoge.txt");
path ==> hoge.txt
jshell> var reader = Files.newBufferedReader(path);
reader ==> java.io.BufferedReader@5a2e4553
varの型推論はforやtry-with-resourcesの構文内の変数宣言でも使用することができる。次の例は、よくあるfor文のカウンタ用変数にvarを利用した例である。このようなケースで変数の型を特別に意識することはあまりないため、varを使うのに適していると言える。
jshell> for (var i = 0; i < 3; i++) {
...> System.out.println(i);
...> }
0
1
2
Java 11におけるvarの機能拡張
varによる型推論はJava 10で導入された機能だが、Java 11でも拡張が行われている。JEP 323として追加された機能で、これはラムダ式の仮引数にvarを使えるようにするというものだ。
次の例は、ラムダ式を使って関数インタフェースを定義したものである。仮引数nameの宣言にvarによる型推論を使用している。この場合、nameはString型として推論される。
jshell> Function <String,String> func = (var name) -> "Hello " + name;
func ==> $Lambda$37/0x0000000800b60040@5ccd43c2
jshell> func.apply("Taro");
$18 ==> "Hello Taro"
ただし、ラムダ式の仮引数についてはもともと型推論が行われており、ここではvarを省略してもString型として扱ってくれる。仮引数にvarが使えるようになって嬉しいのは、アノテーションを利用できるようになる点である。たとえば、仮引数nameに対して@NotNullアノテーションを指定したい場合、次のように書くことができる。
jshell> Function <String,String> func = (@NotNull var name) -> "Hello " + name;
もしvarが使えなかったとすると、アノテーションを利用するためには明示的な型の指定(この場合はString)が必要になる。
今回は、varによる型推論の基本的な使い方を解説した。次回は、型推論が使えないケースや、使うべきではないケースなどについて説明したい。