原文はこちら。
The original article was written by Brian Goetz and Stuart W. Marks.
https://openjdk.org/projects/amber/guides/lvti-faq
Q1. Why have var
in Java?(なぜJavaにvar
?)
ローカル変数はJavaの主役です。ローカル変数を使うと、メソッドが中間値を安いコストで格納することにより、重要な結果を計算できます。フィールドとは異なり、ローカル変数は同一ブロック内で宣言、初期化、利用されます。コードを理解する上で、ローカル変数の名前やイニシャライザは、ローカル変数の型よりも重要であることがよくあります。一般に、名前とイニシャライザは型と同じように多くの情報を持っています。
Person person = new Person();
ローカル変数の宣言におけるvar
の役割は、型の代わりとなって名前とイニシャライザが目立つようにすることです。
var person = new Person();
Javaコンパイラはイニシャライザから変数の型を推論します。これは、型がワイルドカードでパラメータ化されていたり、型がイニシャライザで言及されている場合に特に価値があります。var
を使うことで、コードの読みやすさを犠牲にせずに簡潔にできますし、時には冗長性を取り除いて可読性を向上させることもできます。
Q2. 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
を持つと推論しています。Person
クラスを変更して、getAge
メソッドを持たないようにした場合、もしくはPerson
以外の型のリストに変更した場合、型推論はコンパイル時のエラーで失敗します。
Q3. 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における場合ほど有用ではないと思われます。
Q4. Won’t bad developers misuse this feature to write terrible code?(ひどい開発者がこの機能を誤用してとんでもないコードを書くのではないでしょうか?)
はい、ひどい開発者はどんなふうにしてもひどいコードを書くことでしょう。機能を差し控えたとしても、ひどいコードを禁止できないでしょう。しかし、適切に利用すれば、開発者は型推論を使ってよりよいコードを記述できます。
var
を使って開発者が良いコードを書く方法の1つは、新しい変数の宣言時のオーバーヘッドを減らすことです。変数宣言のオーバーヘッドが大きいと、開発者は変数の宣言を避け、複雑なネスト式やチェーン式を作成して、変数の宣言を増やさないだけのために、可読性を低下させることがよくあるのです。しかしvar
を使うと、名前付き変数に部分式を取り込む際のオーバーヘッドが小さくなるため、開発者はそうする可能性が高くなり、結果として、よりきれいにファクタリングされたコードを作成することができます。
ある機能が導入されると、まず最初はプログラマーがその機能を使用したり、過度に使用したり、悪用したりすることさえあるでしょう。が、それはよくあることです。どんな使い方が妥当で、どんな使い方が妥当でないかのガイドラインがコミュニティに集まるには、ある程度の時間が必要です。ローカル変数の宣言の大部分ではないにしろ、かなり頻繁にvarを使用することは妥当と思われます。
ローカル変数の型推論[Local Variable Type Inference (LVTI)]から、機能提供開始時期にあわせてこのFAQやLVTI Style Guidelinesのような、その意図と推奨する利用方法に関する資料を公開しています。
Local Variable Type Inference Style Guidelines
https://openjdk.org/projects/amber/guides/lvti-style-guide
https://logico-jp.io/2022/07/30/local-variable-type-inference-style-guidelines
こうした取り組みがコミュニティによる合理的なvar
の利用方法の収束を加速し、ほとんどのvar
の乱用を避ける手助けになることを願っています。
Q5. 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
をサポートすることも可能でしたが、簡単のためにこのように型推論のスコープを制限することにしました。いくつかのフィールドといくつかのメソッドの戻り値を含むように境界を広げようとすると、この機能はかなり複雑で難しくなるにも関わらず、有用性はそれほど向上しないからです。
Q6. Why is an initializer required on the right-hand side of var
?(イニシャライザがvar
の右辺に必要な理由は?)
変数の型は、イニシャライザの型から推測されます。これはもちろん、varはイニシャライザがあるときだけ使えるということです。変数への代入から型を推定することもできますが、そうすると機能がかなり複雑になり、誤解を招いたり、診断しにくいエラーになる可能性があります。そこで、シンプルにするために、var
を定義して、ローカルな情報のみを型推論に使用するようにしました。
仮に、変数宣言とは別の複数の場所で、代入に基づく型推論を許可したとします。例えば以下のような例です。
var order;
...
order = "first";
...
order = 2;
(例えば)最初の代入を基準に型が選択された場合、エラーの原因からかなり離れた別の文でエラーが発生する可能性があります(これは「action-at-a-distance (遠隔作用)」問題とも呼ばれることがあります)。
あるいは、すべての代入に対応する型を選択することもできます。この場合、推測される型はStringとIntegerの共通のスーパークラスであるObject
であると予想されます。しかし残念ながら、状況はもっと複雑です。String
とInteger
は両方ともSerializable
にしてComparable
なので、共通のスーパータイプは次のような奇妙な交差型になります(この型の変数を明示的に宣言できないことに注意してください)。
Serializable & Comparable<? extends Serializable & Comparable<...>>
また、この結果、2がorderに代入されるときにボクシング変換 (boxing conversion) が発生し、予想外の望ましくないことが起こるかもしれないことに注意してください。
これらの問題を避けるため、明示的なイニシャライザを使った型推論を要求したほうがよいと思われます。
Q7. Why can’t you use var
with null
?(null
と一緒にvar
を使ってはいけない理由は?)
以下のような宣言を考えてみましょう(これは誤りです)
var person = null; // ERROR
null
リテラルは、Javaのすべての参照型のサブタイプである特殊なnull
型(JLS 4.1)の値を示します。
4.1. The Kinds of Types and Values – The Java® Language Specification – Java SE 11 Edition
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型の変数が必要な場合、明示的に宣言する必要があります。
Q8. Can you use var
with a diamond on the right-hand side?(右辺でダイアモンド演算子と一緒にvar
を利用できる?)
はい、可能ですが、期待されているようなものでない可能性があります。例えば以下の例を考えます。
var list = new ArrayList<>();
この場合、listの型がArrayList<Object>
であると推論されます。一般的には、右側でダイヤモンドを使う場合は左側で明示的な型を、左側でvarを使う場合には右側には明示的な型を、それぞれ使用するのが望ましいとされています。詳細については、LVTI Style GuidelinesのG6を参照してください。
Local Variable Type Inference Style Guidelines
G6. Take care when using var with diamond or generic methods.
https://openjdk.org/projects/amber/guides/lvti-style-guide#G6
https://logico-jp.io/2022/07/30/local-variable-type-inference-style-guidelines/#G6