【Modern Java】Local-Variable Type Inference

f:id:Naotsugu:20200724174249p:plain

blog1.mammb.com


JEP 286: Local-Variable Type Inference

Java 10 によりローカル変数宣言で型推論(type inference)が可能となり var により型定義を省略できるようになりました。

C++ での auto、C# での var、Scala での var/val、Go での := のように大抵の言語ではローカル変数型推論をサポートしており、Java は2018年になりようやく使えるようになりました。

以下のようなローカル変数宣言が可能になります。

var reader = Files.newBufferedReader(...);
var stringList = List.of("a", "b", "c");

final var とすることで再代入不可にすることができます。

final var string = "hello";

コンパイル後のバイトコードレベルでは、従来型の型宣言との違いはありません。


var が使える箇所は以下となります。

  • 初期化子有りのローカル変数宣言
  • 拡張 for ループのインデックス
  • 従来型 for ループのインデックス変数宣言

クラスフィールドやメソッド引数には使えませんし、右辺の型が推測できないような初期化には利用できません。 具体的には以下の箇所で var は使用できません。

  • クラスフィールド
  • メソッド仮引数
  • コンストラクタ仮引数
  • メソッド戻り値型
  • catch 仮引数


さらに、以下のようなラムダ式では使えません。

// NG
var runner = () -> {  };

以下のようにすれば使えますが、

// OK
var runner = (Runnable) () -> {  };

素直に以下のように書いた方がわかりやすいですね。

Runnable runner = () -> {  };


ローカル変数宣言型推論の注意点

例えば以下の記載は何を行っているのかが不明瞭です。

var results = userService.find(q);

この場合は以下のように型を明示した方がわかりやすいでしょう。

List<User> results =  userService.find(q);

なんでもかんでも var を使うのは良くない使い方であり、ガイドラインとしては Local Variable Type Inference: Style Guidelines があります。

4つの原則
  • 書きやすさより読みやすさを常に優先せよ
  • 型推論は局所的なスコープで使い明確なコードにせよ
  • IDE に依存した可読性はさけよ
  • 明示的な型宣言と常にトレードオフを考えよ
7つのガイドライン
  • 意味を伝える変数名を選択する
  • ローカル変数のスコープは出来るだけ小さく
  • 初期化子から十分な意味が読み取れる場合に var を使う
  • メソッドチェーンやネストされた式は var によ中間変数で分割する
  • 「インターフェースを使ったプログラミング」についてはローカル変数では重視しなくてよい
  • ダイヤモンド<> やジェネリックメソッドにおける var 利用は注意すること
  • リテラルにおける var 利用は注意すること

いくつか分かりにくいものもあるので以下に説明します。


「インターフェースを使ったプログラミング」についてはローカル変数では重視しなくてよい

インスタンスは通常該当するインターフェースで受けます。

List<String> list = new ArrayList<>();

しかし var の場合は実装型が推論されます。

var list = new ArrayList<String>();

listArrayList<String> として推論されることになります。

これは「インターフェースを使ったプログラミング」に反するようですが、var はローカル変数にのみ利用でき、通常は狭いスコープでしか使わないため変更などによる後続コードへの影響は限定的です。


ダイヤモンド<> やジェネリックメソッドにおける var 利用は注意すること

以下の例はいずれも PriorityQueue<Item> を扱うので問題ありません。

PriorityQueue<Item> itemQueue = new PriorityQueue<>();
var itemQueue = new PriorityQueue<Item>();

しかし以下の場合はPriorityQueue<Object>() と型推論されるので注意が必要です。

var itemQueue = new PriorityQueue<>();


リテラルにおける var 利用は注意すること

以下のリテラルを var に変えた場合、

byte flags = 0;
short mask = 0x7fff;
long base = 17;

以下は全て int に推論されます。

var flags = 0;
var mask = 0x7fff;
var base = 17;


var の利用例

以下の例は初期化子から十分な意味が読み取れます。

try (InputStream is = socket.getInputStream();
     InputStreamReader isr = new InputStreamReader(is, charsetName);
     BufferedReader buf = new BufferedReader(isr)) {
    return buf.readLine();
}

このようなコードは var を使うことでノイズが減り、可読性が向上します。

try (var inputStream = socket.getInputStream();
     var reader = new InputStreamReader(inputStream, charsetName);
     var bufReader = new BufferedReader(reader)) {
    return bufReader.readLine();
}


ジェネリクスによるメソッドの柔軟性は、結果として冗長となり可読性が悪くなることがあります。

void removeMatches(Map<? extends String, ? extends Number> map, int max) {
    for (Iterator<? extends Map.Entry<? extends String, ? extends Number>> iterator =
             map.entrySet().iterator(); iterator.hasNext();) {
        Map.Entry<? extends String, ? extends Number> entry = iterator.next();
        if (max > 0 && matches(entry)) {
            iterator.remove();
            max--;
        }
    }
}

var により冗長な型宣言を無くすことで可読性が向上します。

void removeMatches(Map<? extends String, ? extends Number> map, int max) {
    for (var iterator = map.entrySet().iterator(); iterator.hasNext();) {
        var entry = iterator.next();
        if (max > 0 && matches(entry)) {
            iterator.remove();
            max--;
        }
    }
}


まとめ

Java 10 で導入されたローカル変数宣言で型推論(type inference)について説明しました。

型情報はコードを読む際の補助になるケースもあれば、ノイズのように感じるケースもあります。

var によるローカル変数宣言の型推論は、常に読みやすさとのトレードオフを意識して注意して使っていく必要がありますね。