Local Variable Type Inference Frequently Asked Questions

原文はこちら。
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であると予想されます。しかし残念ながら、状況はもっと複雑です。StringIntegerは両方とも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

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト /  変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト /  変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト /  変更 )

%s と連携中