[Java] Style Guidelines for Local Variable Type Inference in Java

原文はこちら。
http://openjdk.java.net/projects/amber/LVTIstyle.html

注意:2018/03/25(JST)時点の内容です。

Introduction

Java SE 10では、ローカル変数の型推論が導入されました。以前は、すべてのローカル変数宣言では、左側に明示的な(マニフェスト)型が必要でした。型推論では、明示的な型を、イニシャライザ(初期化子)を持つローカル変数宣言の予約型の名前 var に置き換えることができます。変数の型は、イニシャライザの型から推測されます。

この機能には様々な論争があり、一方ではこの機能が可能にする簡潔性を歓迎する声があるのに対し、他方では、コードを読む人にとって重要な型情報が奪われ、読みやすさが損なわれる恐れがある、という声もあります。いずれのグループの声も間違ってはいません。冗長な情報を排除してコードをより読みやすくすることもできれば、有益な情報を逃すことでコードを読みにくくすることもできます。他には、 var を過度に使用した結果、より悪いJavaコードが書かれることを懸念するグループもいます。これも正しい指摘ではありますが、より良いJavaコードが書かれる可能性もあります。すべての機能と同様に、判断して使用する必要があります。いつ利用すべき、べからずの一般規則はありません。

ローカル変数宣言は単独では存在しません。 周囲のコードによって、 var を使用した場合の影響に影響が及んだり、圧倒されたりする可能性があります。 このドキュメントの目的は、周囲のコードが var 宣言に与える影響を調べ、トレードオフを説明し、 var を効果的に使用するためのガイドラインを提供することです。

Principles

P1. Reading code is more important than writing code.

コードは書かれるより読まれることのほうがずっと多く、さらに、コードを書くとき、私たちは通常、頭の中には全体のコンテキストがあり、時間を掛けますが、コードを読んでいるときには、コンテキスト・スイッチが頻繁に行われ、もっと急いでいる可能性があります。特定の言語機能が使用されているかどうかは、元のコード記述者ではなく、プログラム読者に与える影響によって決定されるべきです。プログラムが短ければ短いほうが望ましいかもしれませんが、短くするとプログラムを理解するのに役立つ情報が省略される可能性があります。ここでの中心的な問題は、わかりやすさを最大化するようにプログラムの適切なサイズを見つけることです。

特にここでは、プログラムの入力や編集に必要なキーボードを叩く量には関心がありません。簡潔さはコード記述者にとっては素晴らしいボーナスになるかもしれませんが、それに注力すると、結果として得られるプログラムのわかりやすさを向上させるという、主要な目的を失ってしまいます。

P2. Code should be clear from local reasoning.

コードの読者は、宣言された変数の利用とともに var の宣言を見て、何が起きているのかをすぐに理解できるはずです。理想的には、スニペットまたはパッチのコンテキストのみを使用して、コードを容易に理解できるようにする必要があります。 var 宣言を理解するためにコードの周りのいくつかの場所を見る必要がある場合は、 var を使うには良い状況ではないかもしれません。そして再度申し上げますが、コード自体に問題がある可能性があります。

P3. Code readability shouldn't depend on IDEs.

IDEでコードを読み書きすることが多いため、IDEのコード解析機能に大きく依存したくなります。型宣言は、型を決める変数を常に指し示すことができるので、どこでも var を使うだけでいいのではないでしょうか。

2つの理由があります。1個目の理由は、コードはIDEの外で読み込まれることが多いということです。コードは、IDE機能が利用できない多くの場所、例えばドキュメント内のスニペット、インターネット上のリポジトリの参照、またはパッチファイルに現れます。コードが何をするのかを理解するためだけに、コードをIDEにインポートする必要があるのは非生産的です。

もう一つの理由は、IDE内でコードを読み込んでいるときでも、変数に関する詳細情報をIDEに問い合わせるために明示的なアクションが必要な場合が多いということです。例えば、 var を使って宣言された変数の型を問い合わせるために、変数上にポインタを置いてポップアップを待たなければならないかもしれません。これはほんの一瞬しか時間がかからないかもしれませんが、コードリーディングの流れを妨害します。

コードは自明でなければなりません。ツールの助けを必要とせずに、その正面から理解できる必要があります。

P4. Explicit types are a tradeoff.

歴史的に、Javaでは、型を明示的に含めるためにローカル変数の宣言が必要でした。明示的な型は非常に役立つことがありますが、時にはそれほど重要ではないこともあり、時には邪魔になることがあります。明示的な型を必要とすると、有用な情報に押し寄せるノイズを増やす可能性があります。

明示的な型を省略すると、ノイズを減少できますが、それは省略してもわかりやすさが損なわれない場合に限ります。型はコードリーダーに情報を伝える唯一の方法ではありません。 他の手段には、変数の名前とイニシャライザ式があります。これらのチャンネルの1つをミュートしても問題ないかどうかを判断するには、使用可能なすべてのチャンネルを考慮する必要があります。

Guidelines

G1. Choose variable names that provide useful information.

これは一般的には良い方法ですが、 var の文脈ではもっと重要です。  var 宣言では、変数の意味と使用に関する情報を変数の名前を使って伝えることができます。明示的な型を var に置き換えるには、変数名を改善する必要があります。 例えば以下のような具合です。
// ORIGINAL
List<Customer> x = dbconn.executeQuery(query);

// GOOD
var custList = dbconn.executeQuery(query);
この例では、役に立たない変数名を、変数の型をイメージできる名前に置き換えました。変数名は、var宣言で暗黙的に記述されています。

変数の型をその名前に記号化して、論理的な結論に達すると、ハンガリアン記法(Hungarian Notation)になります。 明示的な型と同様に、これは時として役に立つこともあれば、混乱することもあります。 この例では、 custList という名前は、 List を返すことを暗示していますが、重要ではないかもしれません。 厳密な型の代わりに、変数名が「顧客(customers)」のよう変数の役目や性質を表現するほうが良い場合もあります。
// 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. Minimize the scope of local variables.

ローカル変数のスコープを制限することは一般的には良い方法です。この方法は、Effective Java(第3版)のItem 57に記述されています。 var を使う場合は余分な力を加えて適用します。

次の例では、addメソッドが特別な項目を最後のリスト要素として明示的に追加するので、期待通り最後に処理されます。
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) ...
このコードにはバグがあります。Setには定義済みの反復順序がないためです。 しかし、プログラマーは items 変数の使用が宣言に隣接しているので、すぐにこのバグを修正する可能性があります。

ここで、このコードが items 変数の大きなスコープを持つ大きなメソッドの一部であるとします。
var items = new HashSet<Item>(...);

// ... 100 lines of code ...

items.add(MUST_BE_PROCESSED_LAST);
for (var item : items) ...
ArrayListからHashSetへの変更の影響は、 items が宣言から遠く離れて使用されるため、もはやわかりづらくなっているため、このバグがずっと長く生き残る可能性があります。

 items が  List<String> として明示的に宣言されている場合は、イニシャライザを変更すれば、型も Set<String> に変更する必要があります。これにより、プログラマは、メソッドの残りの部分を調べて、そのような変更の影響を受けるコードを調べることができます(そして再度、そうならない場合もあります)。  var を使うとこの教唆がなくなるため、このようなコードでバグが導入される危険性が増す可能性があります。

これは var を使用していることに対する議論のように思えるかもしれませんが、実際はそうではありません。 var を使用する最初の例は全く問題ありません。この問題は、変数のスコープが大きい場合にのみ発生します。このような場合に単に var を避けるのではなく、ローカル変数のスコープを狭めるようにコードを変更すべきで、その後に var で宣言するだけです。

G3. Consider var when the initializer provides sufficient information to the reader.

ローカル変数はコンストラクタで初期化されることが多く、コンストラクトされたクラスの名前は左側で明示的な型として頻繁に繰り返されます。型の名前が長い場合、 var を使用して情報を失わずに簡潔にすることができます。
// ORIGINAL
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

// GOOD
var outputStream = new 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. Use var to break up chained or nested expressions with local variables.

文字列のコレクションを取り、最もよく発生する文字列を発見するというコードを考えてみましょう。以下のような感じです。
return strings.stream()
              .collect(groupingBy(s -> s, counting()))
              .entrySet()
              .stream()
              .max(Map.Entry.comparingByValue())
              .map(Map.Entry::getKey);
このコードは正しいですが、単一のストリームパイプラインのように見えるため、混乱を招く可能性があります。実際、それは短いストリームであり、第1のストリームの結果に対する第2のストリーム、続いて第2のストリームの Optional の結果のマッピングが続きます。最も読みやすいこのコードの表現は、2、3行のステートメントでしょう。最初にグループエントリをマップにいれ、続いてそのMapを縮小した後に、(存在する場合は)結果からキーを取り出します。以下のような感じです。
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);
しかし、中間変数の型を書くことはあまりにも負担に思えるので、コード記述者はそうすることをやめて、そのかわりにコントロール・フローを歪めたのです。 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 の使用は実行可能なアプローチですが、2番目のスニペットで中間変数の完全な宣言を使用すると、それは合理性の欠ける選択肢になります。 他の多くの状況と同様に、 var を正しく使用するには、何かを取り除く(明示的な型)と何かを追加する(より良い変数名、コードのより良い構造化)の両方が必要かもしれません。

G5. Don't worry too much about "programming to the interface" with local variables.

Javaプログラミングの一般的なイディオムは、具象型のインスタンスを構築し、それをインタフェース型の変数に割り当てることです。これは、コードを実装(implementation)の代わりに抽象(abstraction)に結びつけるため、将来コードを保守する際の柔軟性が保持されます。
// ORIGINAL
List<String> list = new ArrayList<>();
しかし、 var を使うと、インターフェースではなく具象型を推論します。
// Inferred type of list is ArrayList<String>.
var list = new ArrayList<String>();
ここで、 var はローカル変数に対してのみ使用できることを再考する必要があります。 var はフィールド型、メソッドパラメータ型、およびメソッド戻り値の型を推測するために使用できません。「インターフェイスに対するプログラミング」の原則は、これまでのコンテキストと同じくらい重要です。

主な問題は、変数を使用するコードが具体的な実装に依存する可能性があることです。変数のイニシャライザが将来変更された場合、推測される型が変更され、結果としてその変数を使用する後続のコードでエラーやバグが発生する可能性があります。

ガイドラインG2で推奨されているように、ローカル変数のスコープが小さい場合、後続コードに影響を与える可能性がある具体的な実装の「漏れ」によるリスクは限定的です。変数が数行離れたコードでのみ使用されている場合は、問題を回避したり、問題発生時に問題を緩和したりするのは簡単です。

この特定のケースでは、 List にはない2つのメソッド、つまり ensureCapacity と trimToSize  が ArrayList にのみ含まれています。これらのメソッドはリストの内容には影響しません。したがって、それらのメソッドの呼び出しはプログラムの正確さに影響しません。これは、インタフェースではなく、具体的な実装である推論型の影響をさらに減らします。

G6. Take care when using var with diamond or generic methods.

 var とダイアモンド演算子の両方の機能を使用すると、既に存在する情報から派生可能な場合に、明示的な型情報を省略できます。では、同じ宣言で両方とも使用できるのでしょうか?

以下の例を考えてみましょう。
PriorityQueue<Item> itemQueue = new PriorityQueue<Item>();
これをダイアモンド演算子や var を使って型情報を失わずに書き直すことができます。
// OK: both declare variables of type PriorityQueue<Item>
PriorityQueue<Item> itemQueue = new PriorityQueue<>();
var itemQueue = new PriorityQueue<Item>();
 var とダイアモンド演算子を使うことは問題ありませんが、推論される型は変わります。
// DANGEROUS: infers as PriorityQueue<Object>
var itemQueue = new PriorityQueue<>();
この推論のために、ダイアモンド演算子はターゲット型(通常は宣言の左側)やコンストラクタの引数の型を利用できます。いずれも存在しない場合、最も広く適用可能な型、通常は Object にフォールバックします。これは通常意図したものではありません。

ジェネリックメソッドは型推論を採用しているので、プログラマが明示的な型引数を提供することはほとんどありません。十分な型情報を提供する実際のメソッド引数がない場合、ジェネリックメソッドの推論はターゲットの型に依存します。 var 宣言では、ターゲットの型がないため、ダイヤモンド演算子の場合と同様の問題が発生する可能性があります。
// 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);
ダイヤモンド演算子やジェネリックメソッドと共に var を使用する場合は、推論された型があなたの意図と一致するように、メソッドまたはコンストラクタの引数が型情報を十分に提供するようにする必要があります。そうできない場合には、同一宣言で var とダイアモンド演算子もしくはジェネリックメソッド両方を使わないでください。

G7. Take care when using var with literals.

プリミティブなリテラルは、 var 宣言のイニシャライザとして使用できます。これらのケースでは var を使用すると、型名が一般に不足するため、大きな利点が得られるとは考えにくいのですが、変数名を整列するなど、 var が有用な場合があります。

boolean、character、 long 、およびstringリテラルに問題はありません。 これらのリテラルから推論される型は正確なので、 var の意味は明白です。
// 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";
イニシャライザが数値、特に整数リテラルの場合、とりわけ注意が必要です。左側に明示的な型がある場合、数値は暗黙的に int 以外の型に拡大または縮小されます。 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;
小数点リテラルはほとんどの場合明確です。
// ORIGINAL
float f = 1.0f;
double d = 2.0;

// GOOD
var f = 1.0f;
var d = 2.0;
 float (浮動小数点)リテラルは、暗黙のうちに double に広げる場合があることの注意してください。 3.0f のような明示的なfloatリテラルを使用して double 変数を初期化するのはやや気が利きませんが、 double 変数が float フィールドから初期化される場合があります。ここで var に関する注意をアドバイスします。
// ORIGINAL
static final float INITIAL = 3.0f;
...
double temp = INITIAL;

// DANGEROUS: now infers as float
var temp = INITIAL;
(実際には、この例はガイドラインG3に違反しています。なぜなら、コードの読者が推論された型を見るために十分な情報がイニシャライザにないからです。)

Examples

このセクションでは、 var を使えば最大のメリットを得られる場所の例をご紹介します。

次のコードは、多くとも max 個の一致するエントリをMapから削除します。メソッドの柔軟性を向上させるためにワイルドカード型の境界を使用しているため、結果としてかなりの冗長性が生じます。残念ながら、これはイテレータ(Iterator)の型がネストされたワイルドカードでなければならず、そのため宣言がより冗長になっています。この宣言は長すぎるため、forループのヘッダーが1行に収まらなくなり、コードが読みにくくなっています。
// ORIGINAL
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 を使うと、ローカル変数のノイジーな型宣言が減ります。この種のループでは、IteratorおよびMap.Entryのローカルの明示的な型を持つ必要はほとんどありません。これにより、forループ制御を1行に収めることができ、はるかに読みやすくなります。
// GOOD
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--;
        }
    }
}
try-with-resourcesを使って、ソケットから1行のテキストを読み取るコードを考えてみましょう。ネットワークおよびI/O APIはイディオムをラップしたオブジェクトを使用します。中間オブジェクトをそれぞれリソース変数として宣言して、後続のラッパーを開くときにエラーが発生したら適切にクローズしなければなりません。これを実現するためのこまでのコードであれば、変数宣言の左右でクラス名を繰り返す必要があり、結果として多くのノイズが発生しました。
// ORIGINAL
try (InputStream is = socket.getInputStream();
     InputStreamReader isr = new InputStreamReader(is, charsetName);
     BufferedReader buf = new BufferedReader(isr)) {
    return buf.readLine();
}
var の利用により、大幅にノイズを削減します。
// GOOD
try (var inputStream = socket.getInputStream();
     var reader = new InputStreamReader(inputStream, charsetName);
     var bufReader = new BufferedReader(reader)) {
    return bufReader.readLine();
}

Conclusion

宣言用途で var を利用すると、混乱が減ることでコードの品質が向上します。そのため、より重要な情報を際立たせることができます。対して、無差別に var を採用すると、状況が悪化する可能性があります。適切に var を利用すれば、よいコードを改善するのに役立ち、理解度を損なわずにコードをより短く、明確にできます。

References

JEP 286: Local-Variable Type Inference
http://openjdk.java.net/jeps/286
Wikipedia: Hungarian Notation
https://en.wikipedia.org/wiki/Hungarian_notation
Bloch, Joshua. Effective Java, 3rd Edition. Addison-Wesley Professional, 2018.
https://www.pearson.com/us/higher-education/program/Bloch-Effective-Java-3rd-Edition/PGM1763855.html

0 件のコメント:

コメントを投稿