Java : var (型推論) のガイドライン
型推論の var をうまく使うと、コードがすっきりして読みやすくなります。
しかし、使いすぎると逆に必要な情報が失われて可読性が下がるのでは…と心配になるかもしれません。
いつ var を使うべきか、使うときの注意点などなど。
本記事では、そんなときの指標となる公式ガイドラインをご紹介します。
概要
型推論 var は、Java 10 から言語仕様として追加されました。
var を使うと、ローカル変数の型宣言がよりシンプルになりコードが読みやすくなります。
上記のページは、公式ドキュメントによる型推論の紹介記事です。
そんなに長くはないので、一読してみることをおすすめします。
それでは具体的な例で見てみましょう。
まずは var を使わない例です。
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ByteArrayOutputStream のインスタンスを生成して、outputStream 変数の初期値に設定する、というシンプルなコードです。
ぱっと見ると、1行に ByteArrayOutputStream が2回も出てきて少し冗長に感じます。
次に var を使う例です。
var outputStream = new ByteArrayOutputStream();
だいぶすっきりしました。
ローカル変数の型が var になっていますが、読みにくさにつながる情報の欠落はとくにないでしょう。
var = ByteArrayOutputStream というのは一目でわかりますしね。
プログラム的には ByteArrayOutputStream と書いても var と書いても動作は変わりません。
単純に、人間がコードを読むときに、読みやすいか・読みにくいかの違いだけです。
公式ガイドライン
はじめに
上記の公式ドキュメントには、簡単にですが var のガイドラインがのっています。
- var はコードを読みやすくすることもあれば、有用な情報が省略されて読みにくくなることもあります。
よく考えて使いましょう。
とのことですね。
さらに、次の記事が紹介されています。
こちらは OpenJDK のサイトにある 型推論 var のガイドラインです。
残念ながら英語のみです。(公式による日本語訳はなさそう)
けっこうしっかりとしたガイドラインなので、可能なら目を通すことをおすすめします。
ここでは簡単に内容をご紹介します。
基本的には、機械翻訳したものを、さらに、かなりざっくりと意訳していきます。
より正確な内容については 原文 をご参照ください。
用語の定義
用語 | 説明 |
---|---|
明示的型宣言 | var を使わない型宣言のことです。 |
暗黙的型宣言 | var を使った型宣言のことです。 |
G1, G2, G3, ... | ガイドライン1, ガイドライン2, ガイドライン3, ... という意味です。 |
原則(Principles)
まずは4つの原則が紹介されています。
-
コードは書くことよりも、読まれることのほうがはるかに多くなります。
よって、読みやすいコードにすることは重要です。 -
コードを読む人が、var による変数宣言とその変数の使われかたを見たときに、その意味を即座に理解できなければなりません。
もし、その変数を理解するためにコード内のいくつかの場所 (例えば別のjavaファイルなど) を見る必要があるのなら、それは var を使うべきではないかもしれません。 -
コードの可読性は IDE に依存すべきではありません。
なぜなら、コードを読むだけのひとは、IDE を使わないことも多いからです。
例えば、インターネット上のリポジトリをウェブブラウザから参照する場合などです。 -
明示的型宣言は、型を明記して分かりやすくします。
var による暗黙的型宣言は、冗長な情報を捨てて分かりやすくします。
この2つはトレードオフです。両立はできません。
G1 : 分かりやすい変数名をつける
これは var に限らず一般的な話でもありますね。
とくに var を使うときは、分かりやすい変数名になるよう気をつけましょう。
// ORIGINAL
List<Customer> x = dbconn.executeQuery(query);
// GOOD
var custList = dbconn.executeQuery(query);
// ORIGINAL
try (Stream<Customer> result = dbconn.executeQuery(query)) {
return result.map(...)
.filter(...)
.findAny();
}
// GOOD
try (var customers = dbconn.executeQuery(query)) {
return customers.map(...)
.filter(...)
.findAny();
}
G2 : ローカル変数のスコープは最小限に
これも var に限らず一般的な話ですね。
次の例では、アイテムに要素を追加しています。
var items = new ArrayList<Item>(...);
items.add(MUST_BE_PROCESSED_LAST);
for (var item : items) ...
しばらくして、アイテムは重複させない、という変更が必要になりました。
そこで、ArrayList から HashSet へと変更しました。
var items = new HashSet<Item>(...);
items.add(MUST_BE_PROCESSED_LAST);
for (var item : items) ...
これでアイテムを重複させないという要求は満たせました。
しかし、これだけではバグがあります。
アイテムは追加した順序を保持する必要があったからです。
上記の例では、HashSet に変更した行と、アイテム追加 (items.add) の行が隣接しているので、この問題にすぐ気づけるかもしれません。
しかし、items 変数のスコープが広くなると問題に気づきにくくなるかもしれません。
var items = new HashSet<Item>(...);
// ... 100 lines of code ...
items.add(MUST_BE_PROCESSED_LAST);
for (var item : items) ...
もし、var ではなく明示的な型宣言をしていた場合は、型宣言も次のように変更する必要があります。
List<Item> items = ...
↓
Set<Item> items = ...
プログラマはこの変更によって、変数 items が影響する範囲を、より広範囲でチェックしようと思いつくかもしれません。
(そうならないかもしれません…)
結局のところは、スコープを小さくするのが一番ということですね。
G3 : 一目で型が推論できれば var を使う
ローカル変数は、コンストラクターを使って初期化されることが多いです。
その場合、var を使うと型の情報が失われることなく簡潔になります。
// ORIGINAL
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
// GOOD
var outputStream = new ByteArrayOutputStream();
var = ByteArrayOutputStream であることは一目でわかりますね。
また、メソッド名から十分に型を推論できるのであれば var を使ってもよいでしょう。
// ORIGINAL
BufferedReader reader = Files.newBufferedReader(...);
List<String> stringList = List.of("a", "b", "c");
// GOOD
var reader = Files.newBufferedReader(...);
var stringList = List.of("a", "b", "c");
G4 : 連鎖したメソッド呼び出しを分割するために var 変数を使う
文字列のコレクションを取得し、最も頻繁に出現する文字列を検索するコードを考えてみましょう。
これは次のようになります。
return strings.stream()
.collect(groupingBy(s -> s, counting()))
.entrySet()
.stream()
.max(Map.Entry.comparingByValue())
.map(Map.Entry::getKey);
このコードは正しいですが、単一のストリームがつながっているように見えるため、混乱を招くかもしれません。
(実際には stream() が2つつながっています)
このコードを読みやすくする1例としては、メソッド呼び出しをすべてつなげずに、いくつかの変数に分割することです。
Map<String, Long> freqMap = strings.stream()
.collect(groupingBy(s -> s, counting()));
Optional<Map.Entry<String, Long>> maxEntryOpt = freqMap.entrySet()
.stream()
.max(Map.Entry.comparingByValue());
return maxEntryOpt.map(Map.Entry::getKey);
しかし、おそらくコードの作者は、中間変数の型である
Map<String, Long> freqMap = ...
Optional<Map.Entry<String, Long>> maxEntryOpt = ...
を書くのに煩わしさを感じたのでしょう。
そのため、読みやすさより書きやすさを優先し、最初のコード例のようにメソッド呼び出しをすべてつなげてしまったのです。
var を使うと、型の煩わしさはだいぶ少なくなります。
var freqMap = strings.stream()
.collect(groupingBy(s -> s, counting()));
var maxEntryOpt = freqMap.entrySet()
.stream()
.max(Map.Entry.comparingByValue());
return maxEntryOpt.map(Map.Entry::getKey);
これなら、このコードの作者も中間変数を使うかもしれませんね。
もちろん、最初のコード例のように、メソッド呼び出しを1つにつなげて記述することを正当に好む人もいるでしょう。
しかし、メソッド呼び出しを長くつなげるよりも分割したほうが読みやすくなることもあります。その場合に var は有効です。
G5 : ローカル変数による「インタフェース・プログラミング」の心配は不要
Java プログラミングでは、具体的なクラスのインスタンスを生成し、それをインタフェース型の変数に代入する、ということをよく行います。
例えば、具体的なクラスである ArrayList を、インタフェースの List として変数に代入します。
// ORIGINAL
List<String> list = new ArrayList<>();
このような場合に var を使うと、変数の型は List ではなく ArrayList として型推論されます。
// Inferred type of list is ArrayList<String>
var list = new ArrayList<String>();
しかし、これはあまり心配する必要はありません。
var はあくまでローカル変数の型宣言でのみ使えます。
クラスフィールドの型、メソッドのパラメーターの型、メソッドの戻り値の型として var を使うことはできません。
ローカル変数は、かなり限定的なスコープです。
ガイドライン G2 にもあるように、ローカル変数のスコープが小さければ、その型に具体的なクラスを使ったとしても問題はないでしょう。
G6 : ジェネリック・クラスやメソッドで、型パラメータを省略するときは注意
次のコードは、ジェネリック・クラスのインスタンスを生成する例です。
PriorityQueue<Item> itemQueue = new PriorityQueue<Item>();
このローカル変数の宣言は、次のように書き換えることができます。
// OK: both declare variables of type PriorityQueue<Item>
PriorityQueue<Item> itemQueue = new PriorityQueue<>();
var itemQueue = new PriorityQueue<Item>();
しかし、ジェネリック・クラスやメソッドの型パラメータを省略するときには注意が必要です。
もし型パラメータを省略すると、var による型推論の結果、型パラメータは Object クラスになります。
これは一般的に意図したものではないでしょう。
// DANGEROUS: infers as PriorityQueue<Object>
var itemQueue = new PriorityQueue<>();
// DANGEROUS: infers as List<Object>
var list = List.of();
ジェネリック・クラスのコンストラクターやメソッドのパラメータとして追加の型情報を提供できるのであれば、意図した型として推論されます。
// OK: itemQueue infers as PriorityQueue<String>
Comparator<String> comp = ... ;
var itemQueue = new PriorityQueue<>(comp);
// OK: infers as List<BigInteger>
var list = List.of(BigInteger.ZERO);
G7 : リテラル表記による型推論に注意
var の型推論は、リテラル表記 による宣言にも使えます。
リテラル表記のうち、
- boolean
- 文字
- long
- 文字列
は問題ありません。
これらの表記から推測される型は明確だからです。
// ORIGINAL
boolean ready = true;
char ch = '\ufffd';
long sum = 0L;
String label = "wombat";
// GOOD
var ready = true;
var ch = '\ufffd';
var sum = 0L;
var label = "wombat";
リテラル表記の 数値 には注意が必要です。
左側に明示的な型を指定すると、数値が暗黙的に拡張されたり狭められたりすることがあります。
しかし、var を使うと、それはすべて int 型に推論されます。
これは意図しない可能性があります。
// ORIGINAL
byte flags = 0;
short mask = 0x7fff;
long base = 17;
// DANGEROUS: all infer as int
var flags = 0;
var mask = 0x7fff;
var base = 17;
浮動小数点 float と double は、暗黙的に拡張できることに注意してください。
// ORIGINAL
float f = 1.0f;
double d = 2.0;
// GOOD
var f = 1.0f;
var d = 2.0;
// ORIGINAL
static final float INITIAL = 3.0f;
...
double temp = INITIAL;
// DANGEROUS: now infers as float
var temp = INITIAL;
おまけ
個人的なガイドライン
公式ガイドラインを踏まえたうえで、個人的なガイドラインです。
あくまで私個人の考えなので、そういう考えもあるんだな程度でご覧ください。
【ガイドライン】
- 基本的に使えるところでは常に var を使う
【理由】
案外、var を使う・使わないの判断は難しいと思っています。
公式ガイドラインがあるとはいえ、極論、コードが読みやすいか・読みにくいかという感覚的なところになるので…
特に多人数で開発しているプロジェクトでは、人によって判断も変わってくるでしょう。
よく var を使う人がいて、あまり var を使わない人もいると、全体としては統一感のかけたコードになりそうです。
それらの問題が、常に var を使うことで解決できます。
いちいち使う・使わないの判断をする手間もはぶけますしね。
さて、そこで問題になるのが、公式ガイドラインにある
- 有用な情報が省略されて読みにくくなる
という点です。
これについては 分かりやすい名前 をつけることで緩和します。
名前とは、ローカル変数名はもちろん、クラス名やメソッド名も含みます。
var reader = Files.newBufferedReader(...);
^^^^^^ ^^^^^^^^^^^^^^^^^
↑名前が大事 ↑名前が大事
var を使う・使わないで悩むよりは、名前づけに悩もう、という方針ですね。
型推論に限らずプログラミング全般において、結局名前が分かりやすいほうがコーディングもしやすくなるものです。
名前はとても大事です。
まとめ
型推論の var を使うと、コードがすっきりと読みやすくなります。
ぜひ、うまく活用して可読性の高いコードにしたいですね。
関連記事
- if文の基本
- while文の基本
- for文の基本
- プリミティブ型 (基本データ型)
- リテラルの表記方法いろいろ
- クラスの必要最低限を学ぶ
- インタフェースの default メソッドとは
- インタフェースの static メソッドの使いどころ
- アクセス修飾子の基本
- 配列 (Array) の使い方
- 拡張for文 (for-eachループ文)
- switch文ではなくswitch式を使おう
- try-with-resources文でリソースを自動的に解放
- テキストブロックの基本
- 列挙型 (enum) の基本
- ラムダ式の基本
- レコードクラスの基本 (Record Class)
- メソッド参照の基本
- シールクラスの基本(Sealed Class)
- 無名変数の使い方