varが使えないケースの例
前々回は、varによるローカル変数の型推論について、基本的な使い方を解説した。その中で繰り返し説明したように、varによる型推論が有効なのは、代入しようとしている値の型がコンパイル時点で明確に決まる場合だけに限定される。既に配列の初期化でvarが使えない例を1つ示したが、そのほかにもvarを使えないさまざまなパターンがある。
例えば、次のように右辺が無い場合は変数が明確な型で初期化されないので型推論は使えない。
jshell> var name;
| エラー:
| ローカル変数nameの型を推論できません
| (初期化子なしで変数に'var'を使用することはできません)
| var name;
| ^-------^
nullで初期化する場合にも、明確な型が推論できないと見なされて、varは使えないようになっている。
hell> var name = null;
| エラー:
| ローカル変数nameの型を推論できません
| (変数初期化子は'null'です)
| var name = null;
| ^--------------^
次のコードは、ラムダ式を使ってRunnableクラスを継承した匿名クラスを作る例である。変数runnerがRunnable型として宣言されていれば、普通にコンパイルすることができる。
jshell> Runnable runner = () -> {System.out.println("doit!");};
runner ==> $Lambda$32/0x0000000800b56440@5aaa6d82
ここで型宣言にvarを使うと、次のようにエラーになる。これは右辺のラムダ式で明示的に型が指定されておらず、推論できないためだ。
jshell> var runner = () -> {System.out.println("doit!");};
| エラー:
| ローカル変数runnerの型を推論できません
| (ラムダ式には明示的なターゲット型が必要です)
| var runner = () -> {System.out.println("doit!");};
| ^------------------------------------------------^
メソッド参照でも同様に、varを使うことができない。次の例は、StringクラスのvaueOf()メソッドへのメソッド参照を変数func2に代入して使用している。func2の型をFunction<Object,String>として宣言してあれば、apply()メソッドを呼び出すことでvalueOf()を実行することができる。
jshell> Runnable runner = () -> {System.out.println("doit!");};
runner ==> $Lambda$32/0x0000000800b56440@5aaa6d82
この型宣言をvarに置き換えようとすると、次のようにエラーになる。メソッド参照を使う場合には、明示的に型を指定しなければならない。
jshell> var func2 = String::valueOf;
| エラー:
| ローカル変数func2の型を推論できません
| (メソッド参照には明示的なターゲット型が必要です)
| var func2 = String::valueOf;
| ^--------------------------^
次の例は、varを使えないというわけではないが、推奨されない使い方となっている。ArrayListのインスタンスを型引数を省略して生成したケースである(このような書き方をダイアモンド記法と呼ぶ)。
jshell> var list2 = new ArrayList<>();
list2 ==> []
jshell> list2.add(123);
$35 ==> true
jshell> list2.add("Hello!");
$36 ==> true<Paste>
この場合、list2はArrayList<Object>に推論される。そのため、list2には複数のデータ型の値を追加することができてしまう。このような使い方はジェネリクスの型安全性のポリシーに反しているため、型引数を明示して使うことが好ましい。
どんなときにvarを使えばよいか
varによる型推論は、うまく使えばプログラムの記述量を減らしてコーディングにかかる時間を短縮することができる。その一方で、それぞれ変数がどの型なのかが目視で判別しづらくなるという問題もある。一般的に、プログラムは書きやすさ容易さよりも読みやすさを大切にするべきだとされている。したがって、varを利用する場合も、プログラムの可読性を低下させないように配慮する必要がある。
可読性を維持するという観点でvarを使っても問題ないケースとしては、次のような場面が挙げられる。
- newによるインスタンス化など、右辺に型名が明記されている
- メソッド呼び出しで、戻り値の型が容易に推測できる
var date = new Date();
var list = new ArrayList<String>();
var fruitsList = List.of("Apple","Orange","Grape"); // 戻り値がList<String>であることが明確
var reader = Files.newBufferedReader(Paths.get("hoge")); // 戻り値がBufferedReaderだろうと推測できる
このような場面では、右辺を見れば人間の目でも型がはっきりと推測できるため、varを使っても可読性が下がる心配はない。ただしその場合でも、どの型か分かりやすい変数名を使ったり、対象の変数のスコープを最小限にするなどといった配慮をするのが望ましい。
逆にvarの使用を控えたほうがいいのはどのような場面だろうか。端的に言えば、「ひと目で型が推測できないケース」はすべてvarを使うべきではない。具体的には、次のような場面ではたとえ機能的にはvarを使うことができたとしても、使用は控えるべきだろう。
- 戻り値の型が名前から推測できないメソッド呼び出し
- プリミティブ型なのかラッパークラスなのか判別しにくい
- ダイアモンド記法でジェネリクスを初期化している(前述)
Path path = Paths.get("hoge.txt");
var filename = path.getFileName(); // 実際の戻り値はPath型だが、Stringのようにも見える
var size = hoge.getSize(); // intやdoubleなのか、それともラッパクラスなのか
var list = new ArrayList<>(); // ダイアモンド記法による初期化
var list = List.of(); // ジェネリクスの型引数が不明(実際はList<Object>)
現実的には、型が見た目で判別できなかったとしても、IDEの機能によって可視化してくれるケースが多い。しかし、一般的にコードの可読性は使用するツールに依存して考えるべきではないため、IDEを前提としてvar使用の可不可を判断しないほうがよいだろう。
そのほか、varを使用する上での注意点については、OpenJDKプロジェクトが公開しているガイドライン「Local Variable Type Inference: Style Guidelines」も参考にしていただきたい。