[Java] Java Local Variable Type Inference: Frequently Asked Questions

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

Why have var in Java?(なぜJavaにvar?)

ローカル変数はJavaの主役です。ローカル変数を使うと、メソッドが中間値を安いコストで格納することにより、重要な結果を計算できます。フィールドとは異なり、ローカル変数は宣言されたブロック内でのみ利用するため、コンパイラは常にローカル変数が利用前に初期化されていると保証できます。コードを理解する上で、ローカル変数の名前や初期値は、ローカル変数の方よりも重要であることがよくあります。型は重要ではありますが、次の行で使う名前のほうが重要なことがあります。varを使うことで、ローカル変数宣言の重要な部分を目立たせることができます。

型ではなくvarを使う場合、Javaコンパイラは初期値から変数の型を推論します。これは特に型が長い名前であったり、複雑なパラメータ化された型、もしくは型が初期値と重複している場合に価値があります。varを使うことで、コードの読みやすさを犠牲にせずに簡潔にできますし、土岐には冗長性を取り除いて可読性を向上させることもできます。

Does this make Java dynamically typed? Is this like var in JavaScript?(これはJavaが動的な型になるということですか?JavaのvarはJavaScriptのvarのようなものですか?)

どちらの質問に対する答えも、Noです。Javaは変わらず静的型言語で、varが加わったからといって変わることはありません。型ではなく、varをローカル変数の宣言で利用可能、というだけです。varを使う場合、Javaコンパイラはコンパイル時に変数のイニシャライザから取得する型情報を使って変数の型を推論します。その後、変数の静的型として推論された型を使います。通常、推論した型は明示的に書いた場合と同じ型のため、varを使って宣言した変数は、型を明示的に記述した場合とまったく同じように正しく振る舞います。

Javaコンパイラは長年にわたって型推論をしてきました。例えばJava 8では、ラムダ式のパラメータには明示的な型を必要としていませんが、これはコンパイラがラムダ式での使われ方から、パラメータの型を推論しているからです。
List<Person> list = ...
list.stream().filter(p -> p.getAge() > 18) ...
上記のコード・スニペットでは、ラムダ式のパラメータ p は静的な型Personを持つと推論しています。getAgeメソッドを持たないようにPersonクラスを変更した場合、もしくはPerson以外の型のリストに変更した場合、型推論はコンパイル時のエラーで失敗します。

Is a var variable final?(varで宣言した変数はfinalですか?)

いいえ。varで宣言したローカル変数はデフォルトでfinalではありません。ただし、final修飾子はvar宣言に追加できます。
final var person = new Person();
Javaではfinal varの短縮はありません。Scalaのような言語ではvalを使ってイミュータブル(final)変数を宣言します。Scalaの場合、全変数(ローカル変数でもフィールドでも)が以下の形式の構文を使って宣言されるため、うまくいきます。
val name : type
もしくは
var name : type
型推論させたいか否かによって、宣言の": type"部分を含めることも、省略することもできます。Scalaの場合、可変性(mutability)と不変性(immutability)の選択が型推論と直交します。

Javaの場合、varは型推論が必要な場合にのみ利用できます。つまり、明示的に型宣言されている箇所では利用できません。valを追加した場合、この場合も型推論を用いる箇所でのみ利用できます。型が明示的に宣言されている場合、Javaでvarもしくはvalの利用によって不変性を制御することはできません。

さらに、Javaではvarをローカル変数に対してのみ利用できます(フィールドは対象外です)。不変性はフィールドではずっと重要ではありますが、イミュータブルなローカル変数は(イミュータブルなフィールド変数に比べると)あまり使われません。

var/val キーワードを使って不変性を制御することはScalaからJavaへきれいに引き継ぐべきであるような機能ですが、JavaにおいてはScalaにおける場合ほど有用ではないと思われます。

Won't bad developers misuse this feature to write terrible code?(ひどい開発者がこの機能を誤用してとんでもないコードを書くのではないでしょうか?)

はい、ひどい開発者はどんなふうにしてもひどいコードを書くことでしょう。機能を保留しても、ひどいコードを禁止できないでしょうしかし、適切に利用すれば、開発者は型推論を使ってよりよいコードを書くことができます。

varの利用によって、新しい変数の宣言のオーバーヘッドが減るため、開発者はよりよいコードを書くことができます。変数の宣言のオーバーヘッドが高いと、開発者はそのオーバーヘッドを避けようとします。そのため、より多くの変数宣言をせずにすむよう、可読性を損なう複雑なネスト、もしくはチェーンになった式を作成するでしょう。varを使うと名前付き変数に部分式(subexpression)を引き渡すオーバーヘッドが少なくなるため、開発者はvarを使うようになるでしょう。その結果、コードがきれいになるでしょう。

機能が導入されると、まず最初はプログラマーがその機能を使用したり、過度に使用したり、悪用したりすることさえあるでしょう。が、それはよくあることです。合理的な利用方法に関するガイドラインにコミュニティが落ち着くまでには時間がかかります。大部分のローカル変数宣言ではなく、かなり頻繁にvarを使用することはおそらく合理的です。

ローカル変数の型推論[Local Variable Type Inference (LVTI)]から、機能提供開始時期にあわせてこのFAQやLVTI Style Guidelinesのような、推奨する利用方法に関する資料を公開しています。
Style Guidelines for Local Variable Type Inference in Java
https://openjdk.java.net/projects/amber/LVTIstyle.html
https://orablogs-jp.blogspot.com/2018/03/style-guidelines-for-local-variable.html
こうした取り組みがコミュニティによる合理的なvarの利用方法の収束を加速し、ほとんどのvarの乱用を避ける手助けになることを願っています。

Where can var be used?(varを利用可能な箇所は?)

var はローカル変数やfor-loopのインデックス変数、try-with-resourcesにおけるリソース変数の宣言目的で利用できます。

ただし、var はフィールドやメソッドパラメータ、メソッドの戻り値で利用することはできません。それは、これらの場所の型は明示的にクラスファイルやjavadoc仕様書に現れているからです。型推論を使えば、イニシャライザへの変更によって変数の推論された型を変更するのはきわめて簡単です。ローカル変数の場合、これは問題ありません。というのも、ローカル変数はスコープが限定されており、ローカル変数の型はクラスファイルに直接記録されていないからです。しかしながら、フィールドやメソッドのパラメータ、メソッドの戻り値の型を推論する場合、型推論では簡単に問題が発生する可能性があります。

例えば、メソッドの戻り値がメソッドのreturn文から推論されたとしましょう。メソッドの実装に対する変更により、return文の式の型が変更される可能性があります。この結果、メソッドの戻り値の型が変わるため、ソースやバイナリの非互換性が発生する可能性があります。このような互換性のない変更は、実装の無害な変更から生じるべきではありません。

推論によりフィールドの型が決まる場合、フィールドのイニシャライザへの変更の結果フィールドの型が変わる可能性があり、結果としてreflective codeを予期せずに破壊する可能性があります。

型推論は実装内ではOKですが、APIではNGです。APIのコントラクトは明示的に宣言すべきです。

APIの一部ではないプライベート・フィールドやメソッドではどうでしょうか?理論的には、従属コンパイルと動的リンクにより互換性を損なう心配がないため、privateフィールドとprivateメソッドの戻り値の型に対してvarをサポートすることも可能でしたが、簡単のためにこのように型推論のスコープを制限することにしました。いくつかのフィールドといくつかのメソッドの戻り値を含むように境界をプッシュしようとすると、そのフィーチャはかなり複雑で難しくなるものの、有用性はそれほど向上しません。

Why is an initializer required on the right-hand side of var?(イニシャライザがvarの右辺に必要な理由は?)

変数の型は、イニシャライザの型から推測されます。これはもちろん、varはイニシャライザがある場合にのみ使用できるということです。変数への代入から型を推測することも可能でしたが、その場合、この機能がかなり複雑になってしまい、誤解を招くか、診断しづらいエラーにつながる可能性があります。物事を単純にするために、ローカル情報だけを使って型推論するようvarを定義しました。

宣言とは別の複数の場所での代入を基にして型推論できたとしましょう。例えば以下のような例です。
var x;
...
x = "foo";
...
x = 17;
(例えば)最初の代入に基づいて型を選択した場合、エラーの原因から遠く離れた別の文でエラーが発生します(これは「遠隔操作」問題とも呼ばれることがあります)。

あるいは、すべての割り当てと互換性のあるタイプを選択することもできますが、この場合、推論される型がObjectであると予想される方がいらっしゃるかもしれません。というのも、ObjectStringIntegerの共通のスーパークラスであるためです。しかし残念ながら、この状況はもっと複雑です。StringIntegerの両方がSerializableにしてComparableであるため、共通のスーパータイプは以下のような奇妙な交差型になるでしょう。
Serializable & Comparable<? extends Serializable & Comparable<...>>
(この型の変数を明示的に宣言できないことに注意してください。)また、17をxに代入すると、予期せず、望ましくないボクシング変換(boxing conversion)が発生します。

これらの問題を避けるため、明示的なイニシャライザを使った型推論を要求したほうがよいと思われます。

Why can't you use var with null?(nullと一緒にvarを使ってはいけない理由は?)

以下のような宣言を考えてみましょう(これは誤りです)
var x = null; // ERROR
nullリテラルは、Javaのすべての参照型のサブタイプである特殊なnull型(JLS 4.1)の値を示します。
The Java® Language Specification - Java SE 11 Edition
The Kinds of Types and Values
https://docs.oracle.com/javase/specs/jls/se11/html/jls-4.html#jls-4.1
null型の唯一の値はnullそのものなので、null型の変数に代入できる唯一の値はnullです。これはあまり役に立ちません。

nullに初期化されたvar宣言がObject型を持つと推測されるように特別な規則を作ることもできました。確かに可能なのですが、プログラマの意図に対する疑問が出てきます。変数は後で他の値に割り当てることができるように、nullに初期化されている場合、変数の型をObjectとして推論するのは正しい選択であるとは考えづらいのです。

このケースを処理するための特別なルールを作らず、禁止することにしました。Object型の変数が必要な場合、明示的に宣言する必要があります。

Can you use var with a diamond on the right-hand side?(右辺でダイアモンド演算子と一緒にvarを利用できる?)

はい、可能です。しかし、おそらくは期待されているようなものではないでしょう。例えば以下の例を考えます。
var list = new ArrayList<>();
この場合、リストの型がArrayList<Object>であると推論されます。一般的には、右側でダイヤモンドを使う場合は左側で明示的な型を、左側でvarを使う場合には右側には明示的な型を、それぞれ使用するのが望ましいでしょう。詳細については、LVTIスタイルガイドラインのガイドラインG6を参照してください。
Style Guidelines for Local Variable Type Inference in Java
G6. Take care when using var with diamond or generic methods.
https://openjdk.java.net/projects/amber/LVTIstyle.html#g6.-take-care-when-using-var-with-diamond-or-generic-methods.
https://orablogs-jp.blogspot.com/2018/03/style-guidelines-for-local-variable.html#G6

0 件のコメント:

コメントを投稿