54
45

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

【JDK 10】varを使った型推論

Last updated at Posted at 2017-12-15

この記事は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フォルダ内のjavacjshellを利用することで、この記事のサンプルソースのように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
jshell> int limit = 20; // 従来の書き方でint型の変数を宣言
limit ==> 20

jshell> var limit = 20; // varを使ってint型の変数を宣言
limit ==> 20

intの型宣言がvarに置き換わっています。

オブジェクト型についても同様です。いくつか見てみましょう。

jshell
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
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
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
jshell> var article = null;

|  エラー:
|  cannot infer type for local variable article
|    (variable initializer is 'null')
|  var article = null;
|  ^--------------^

というように、nullでは型推論ができない旨のエラーが表示されます。

ラムダ式とは併用できない

次にvarが使えない状況として、ラムダ式により右辺の型を省略している場合があります。

例えば以下のようなRunnableクラスを継承した匿名クラスを作る場合、現行バージョンのJavaではラムダ式をして以下のように記述できます。

jshell
jshell> Runnable runner = () -> { System.out.println("run!"); };

runner ==> $Lambda$15/1471868639@343f4d3d

一方で、新しいvarを使ってこれを書こうとすると、以下のようになります。

jshell
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
jshell> var runner = (Runnable) (() -> {System.out.println("run!");})
runner ==> $Lambda$15/87765719@5442a311

とはいえ、これならvarを使わずに左辺で型を指定したほうが見やすいコードになるため、わざわざこのように書く必要はないでしょう。

jshell
jshell> Runnable runner = () -> {System.out.println("run!");};
runner ==> $Lambda$15/87765719@5442a311

インスタンス変数では使えない

先述したとおり、varは「ローカル変数」にしか使えません。つまり、以下のようにインスタンス変数に使おうとするとエラーになります。

jshell
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
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

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を型の前につけるだけです。

VarSample.java
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は、右辺から推論したとおりの型で右辺を受けうけとります。

つまり、今まで決まり文句のように書いていた、生成した具象クラスをインターフェースで受け取るということはできません。

jshell
// 生成したArrayList<String>のインスタンスを、インターフェースであるList<String>で受け取る
jshell> List<String> idList = new ArrayList<>();  
jshell
// varを使うと受け取る型を指定することができない
jshell> var idList = new ArrayList<String>();  

「オブジェクトはなるべくインターフェースで扱え!」という習慣が身についていると、これはとても大変な変更のように一瞬思ってしまいますが、実際はこれで不都合がでることはほとんどありません。

確かに、具象クラスで受け取ってしまうと以下のように、同じインターフェースを持つ別のクラスを代入する場合に不都合が発生します。

jshell
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
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
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というリポジトリを紹介していますので、「それ自分だ!」と思った方はぜひ読んでみてください。

54
45
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
54
45

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?