この記事はJava Advent Calendar 2017の16日目です。
JDK 10 で追加される予定のJEP 286: Local-Variable Type Inferenceについて、その内容と使い方について考えてみます。
(2018.03.22 追記) JDK10正式版での動作確認について
2018年3月20日に、JDK10の正式版がリリースされましたので、この記事を書いたころから何か変更が入っていないか確認しました。
C:\Users\chooyan>"C:\Program Files\java\jdk-10\bin\javac.exe" -version
javac 10
結果、動作確認したり以下のページを見る限り、特にこの記事に記載した内容については変更無しのようでした。
Java 10 Local Variable Type Inference | Oracle Developers
なお余談ですが、この記事のコメント欄で話題になったvar list = new ArrayList<>();
と書いてしまった場合の挙動について、上の記事でも触れられています。
結論としてはArrayList<Object>
という扱いになってしまってメリットは特にないので避けてね、とのことです。
(追記ここまで)
この記事の対象読者
この記事は、
- Javaを使って仕事をしていて
- まだJEP 286の内容は未チェックで
- Java以外の言語はあまり経験がない
という方を想定した記事です。
var
はJava以外の言語でもよく出てくるキーワードのため、それらとの比較を期待される方もいるかと思いますが、そのあたりを書けるほどの知識と経験が僕にないため、この記事ではJavaのみに着目して新しく導入される予定のvar
を紹介するような内容となっています。
Javaのvar
についてざっくりと予習しておきたい方にとって最適な記事になるように書いていますので、それを踏まえた上で先を読み進めていただければと思います。
使用するJDK
この記事ではJDK 10 Early-Access Buildsを利用します。
JDKをダウンロード・インストールし、bin
フォルダ内のjavac
やjshell
を利用することで、この記事のサンプルソースのようにvar
を利用したソースコードを試すことができます。
C:\Users\chooyan>"C:\Program Files\java\jdk-10\bin\javac.exe" -version
javac 10-ea
varの概要
JEP 286: Local-Variable Type Inferenceで検討されているのは、変数宣言時の型指定にvar
を使うことで、変数宣言のためのコードを簡略化できるようにしましょう、というものです。
あれこれ説明する前に、サンプルコードを見てみましょう。なお、この記事では、数行で完結するサンプルコードにはjshell
を使います。
以下はint型の変数を宣言する例です。
jshell> int limit = 20; // 従来の書き方でint型の変数を宣言
limit ==> 20
jshell> var limit = 20; // varを使ってint型の変数を宣言
limit ==> 20
int
の型宣言がvar
に置き換わっています。
オブジェクト型についても同様です。いくつか見てみましょう。
jshell> String name = "chooyan_eng"; // 従来の書き方でString型の変数を宣言
name ==> "chooyan_eng"
jshell> var name = "chooyan_eng"; // varを使ってString型の変数を宣言
name ==> "chooyan_eng"
jshell> List<String> idList = new ArrayList<>(); // 従来の書き方でList<String>型の変数を宣言
idList ==> []
jshell> var idList = new ArrayList<String>(); // varを使ってList<String>型の変数を宣言
idList ==> []
また、右辺がメソッドの場合も、そのシグニチャから戻り値の型が明確ですのでvar
が使えます。
jshell> private int sum(int x, int y) {
...> return x + y;
...> }
| 次を作成しました: メソッド sum(int,int)
jshell> var s = sum(4, 5);
s ==> 9
いかがでしょうか。上記のように、__左辺の型指定をvar
に置き換えることで、タイプ量を減らしてコードを読みやすくする__というのがJEP286で検討されているJavaの新しい型推論です。
varの制限
さて、新しいvar
を使うことで、左辺の型指定を簡略化できると書きましたが、いつでもvar
が使えるという訳ではありません。
var
を使うためには、以下の条件を満たしている必要があります。
- メソッド内で宣言されるローカル変数である
- 右辺を見れば型が明白である
前提として、Javaのvar
は__「右辺を見れば左辺の型はコンパイル時に判別できるから、コーディング時には左辺の型指定を省略できる」__というものです。「実行時に型を判断する」という仕組みではありません。
もう少し詳しく説明すると、たとえばArticle
クラスのインスタンスを生成してarticle
変数に代入する場合、今までは
jshell> Article article = new Article();
article ==> Article@59494225
と書いていました。よく見ると、右辺で生成したインスタンスがArticle
型である以上、それを代入するための変数の型もArticle
型(もしくはその親クラス)でなければなりません。そして、そうでない場合にエラーにする、というのは現在もコンパイラが判断していることです。
であれば、左辺にはvar
とだけ書いておいて、実際の変数の型はコンパイラが推論できるようにしちゃえば、1行に2回同じクラスを書く必要がなくなって便利だよね、というのがJavaのvar
の考え方です。
逆に、コンパイル時に右辺を見ても型が明確に決まらないような場面ではvar
が使えません。ここからは、どのような場合に「右辺を見ても型が明確に決まらない」のかについて順番に説明していきます。
右辺を見ただけでは型が判断できない場合
右辺を見ただけでは型が判断できない場合はいくつかあります。
右辺がnull
変数宣言時、とりあえずnull
を代入しておく(もしくは何も代入しない)という状況はそれほど珍しくないのではないかと思います。
例えば
private Article getArticle(int id) {
Article article = null;
if (isOnline()) {
article = requestArticle(id);
} else {
article = selectArticleFromCache(id);
}
// article変数に対する何らかの処理
return article;
}
というように、if文の中で代入する方法を変えたい、でもif文の外でもその変数にアクセスしたい、という場合には、if文の前でarticle
変数を宣言し、nullで初期化する必要があります。
しかし、このとき
Article article = null;
という変数宣言を
var article = null;
と書くことはできません。なぜなら、この一行だけでは右辺がnullなため、変数article
に何の型が代入されるのかをコンパイラが判断できないからです。
実際にjshellで試してみると、
jshell> var article = null;
| エラー:
| cannot infer type for local variable article
| (variable initializer is 'null')
| var article = null;
| ^--------------^
というように、nullでは型推論ができない旨のエラーが表示されます。
ラムダ式とは併用できない
次にvar
が使えない状況として、ラムダ式により右辺の型を省略している場合があります。
例えば以下のようなRunnable
クラスを継承した匿名クラスを作る場合、現行バージョンのJavaではラムダ式をして以下のように記述できます。
jshell> Runnable runner = () -> { System.out.println("run!"); };
runner ==> $Lambda$15/1471868639@343f4d3d
一方で、新しいvar
を使ってこれを書こうとすると、以下のようになります。
jshell> var runner = () -> { System.out.println("run!"); };
| エラー:
| cannot infer type for local variable runner
| (lambda expression needs an explicit target-type)
| var runner = () -> { System.out.println("run!"); };
| ^--------------------------------------------^
右辺のラムダ式に明示的な型指定がないため、左辺のvar
は型を判断できないよ、というメッセージがでています。つまり、ラムダ式による型の省略とvar
は同時に使えない、ということですね。
ちなみに、このようなケースは、以下のように型の変換を明示的に行うことで一応エラーを回避できます。
jshell> var runner = (Runnable) (() -> {System.out.println("run!");})
runner ==> $Lambda$15/87765719@5442a311
とはいえ、これならvar
を使わずに左辺で型を指定したほうが見やすいコードになるため、わざわざこのように書く必要はないでしょう。
jshell> Runnable runner = () -> {System.out.println("run!");};
runner ==> $Lambda$15/87765719@5442a311
インスタンス変数では使えない
先述したとおり、var
は「ローカル変数」にしか使えません。つまり、以下のようにインスタンス変数に使おうとするとエラーになります。
jshell> class Article {
...> var id = 0;
...> var title = "";
...> }
| エラー:
| 'var' is not allowed here
| var id = 0;
| ^-^
| エラー:
| 'var' is not allowed here
| var title = "";
| ^-^
エラーメッセージもそのままですね。「ここでは使えないよ」ということだそうです。
詳しくは分かりませんが、スコープが広くなってしまうと判断が難しくなるからローカル変数のみの制限になっている、ということでしょうか。このあたりは未調査ですが(すみません)、そういうものだそうです。
型宣言を省略した配列の初期化には使えない
以下のように、型宣言を省略して配列を作成する場合、var
を使うことができません。
jshell> var arr = {1, 2, 3}
| エラー:
| cannot infer type for local variable arr
| (array initializer needs an explicit target-type)
| var arr = {1, 2, 3};
| ^------------------^
一方でnew int[]
で型を指定した場合は問題ありません。
jshell> var arr = new int[]{1, 2, 3}
arr ==> int[3] { 1, 2, 3 }
タイプ量としてはvar
を使わない方が少ないですが、ここは後者のような、型指定をした上でvar
で宣言するのが、他の宣言方法との統一性があってよいような気がしました。
var
導入に伴うFAQ
その他、var
導入に伴って疑問に思われる(と思われる)内容を、一問一答形式で考えてみたいと思います。
Q1. final
にしたい場合はどうするの?
例えばKotlinの場合、再代入できる変数はvar
を、再代入できない変数にはval
を、というような使い分けがあります。
Javaではそのような「再代入できないvar
」をあらわすものは特に今のところ用意されていません。今まで通りfinal
を型の前につけるだけです。
public class VarSample {
public static void main(String[] args) {
final var id = 2;
id = 3;
System.out.println(id);
}
}
上記のコードをコンパイルすると、ちゃんとfinal
をつけた変数には代入ができなくなっていることが分かります。
$ javac VarSample.java
VarSample.java:4: エラー: final変数idに値を代入することはできません
id = 3;
^
じゃあ定数もpublic static final var
と書けばよいのか、という話ですが、これはそもそもvar
がローカル変数にしか使えないという制限がありますので、定数は今まで通り
public static final String DEFAULT_ID = "9999";
のように記述します。
ローカル変数につけられる修飾子はfinal
だけですので、その他の修飾子との兼ね合いについて考えることはおそらくないでしょう。
Q2. インターフェースで受け取りたいんだけど
var
は、右辺から推論したとおりの型で右辺を受けうけとります。
つまり、今まで決まり文句のように書いていた、生成した具象クラスをインターフェースで受け取るということはできません。
// 生成したArrayList<String>のインスタンスを、インターフェースであるList<String>で受け取る
jshell> List<String> idList = new ArrayList<>();
// varを使うと受け取る型を指定することができない
jshell> var idList = new ArrayList<String>();
「オブジェクトはなるべくインターフェースで扱え!」という習慣が身についていると、これはとても大変な変更のように一瞬思ってしまいますが、実際はこれで不都合がでることはほとんどありません。
確かに、具象クラスで受け取ってしまうと以下のように、同じインターフェースを持つ別のクラスを代入する場合に不都合が発生します。
jshell> var idList = new ArrayList<String>();
idList ==> []
jshell> idList = new LinkedList<String>();
| エラー:
| 不適合な型: java.util.LinkedList<java.lang.String>をjava.util.ArrayList<java.lang.String>に変換できません:
| idList = new LinkedList<String>();
| ^----------------------^
しかし、もともとこのようなコードを書くことはほとんどないはずです。
そもそもインターフェースで扱うのは、メソッドの引数や戻り値としてどの具象クラスでも同じように扱えるようにする(必要に応じて具象クラスを切り替えられるようにする)ためです。
そして、そのような使い方はvar
でも変わらず可能です。
jshell> private List<String> getIdList() {
...> if (isArrayListSuitable()) {
...> return new ArrayList<String>();
...> } else if (isLinkedListSuitable()) {
...> return new LinkedList<String>();
...> }
...> return new ArrayList<String>();
...> }
| 次を作成しました: メソッド getIdList()
jshell> var idList = getIdList();
idList ==> []
結局、var
が自動的に具象クラスで受け取るようになるからと言っても、ローカル変数の宣言時にしか利用できない限り影響は限定的である、ということになります。
Q3. すでにvar
って名前のクラス作っちゃったんだけど
これは問題です。なにが問題かというと、"var"という、Javaの命名規則から外れたクラス名を作ってしまったことが問題です。
実はJEP286でも、var
を導入するリスクの一つとして"var"クラスがすでに定義されていた場合の互換性の問題が挙げられています。
Risk: source incompatibilities (someone may have used var as a type name.)
しかし、そもそもクラス名として小文字始まりの単語、さらには他の言語でキーワードとして頻繁に使われる"var"という単語を型名としてつけることはほとんどないはずだから、"var"を新しい予約語とする方向は変わらない旨が書かれています。
Mitigated with reserved type names; names like var do not conform to the naming conventions for types, and therefore are unlikely to be used as types. The name var is commonly used as an identifier; we continue to allow this.
すでにvar
というクラスを作ってしまった場合は、なんとかJDK 10に移行する前にリネームする必要があります。
なお、変数名の"var"は特に問題がありません。
少し読みづらいですが、キーワードのvar
と併せて使うことができます。
jshell> int var = 1; // int型の変数varを定義
var ==> 1
jshell> var = 2; // 変数varに別の値を代入
var ==> 2
jshell> var var = 1; // varを使って変数varを定義(int型が推論される)
var ==> 1
まとめ
以上、JDK 10で導入される予定のvar
について、現在リリースされているJDK10のearly access版を利用して確認してみました。
JavaScript等の言語をやっている人にとっては、var
と聞くとJavaが動的に型が変化する言語になるような印象を受けるかもしれませんが、Javaのvar
とは「左辺の型指定を省略できる」程度のもので、特に挙動が変わるものではないことが伝わったのではないかと思います。
var
が使えるようになると、今までとは違ったコーディング規約や慣習が出てくることが予想されますので、今のうちからイメージしておくことでスムーズに受け入れることができるのではないかと思います。ぜひJDK10のearly access版を落として動かしてみてください。
なお、まだ僕もJEP 286に記載されている内容をちゃんと読んで理解したわけではないため、内容に一部誤りがあるかもしれません。何か変な点があればコメントいただければと思います。
宣伝
先日、これからRubyで仕事したい人のためのリポジトリ「Code Your Ruby」を作りましたという記事を書きました。
「今はJavaエンジニアだけど、これからRubyを仕事にしてみたい」 という方の勉強に最適なCode Your Rubyというリポジトリを紹介していますので、「それ自分だ!」と思った方はぜひ読んでみてください。