事実上最終対最終-異なる動作


104

これまでのところ、実質的にfinalfinalはほぼ同等であり、JLSは、実際の動作が同一でなくても、それらを同様に扱うと思いました。それから私はこの不自然なシナリオを見つけました:

final int a = 97;
System.out.println(true ? a : 'c'); // outputs a

// versus

int a = 97;
System.out.println(true ? a : 'c'); // outputs 97

どうやら、JLSはここで2つの間に重要な違いをもたらし、その理由はわかりません。

私は次のような他のスレッドを読みました

しかし、それらはそのような詳細には入りません。結局のところ、より広いレベルでは、それらはほとんど同等であるように見えます。しかし、深く掘り下げると、明らかに異なります。

この動作の原因は何ですか?誰かがこれを説明するいくつかのJLS定義を提供できますか?


編集:私は別の関連するシナリオを見つけました:

final String a = "a";
System.out.println(a + "b" == "ab"); // outputs true

// versus

String a = "a";
System.out.println(a + "b" == "ab"); // outputs false

したがって、文字列のインターンもここでは異なる動作をします(このスニペットを実際のコードで使用したくないので、異なる動作に興味があります)。


2
非常に興味深い質問です!Javaはどちらの場合も同じように動作することを期待しますが、今では啓発されています。これが常に動作だったのか、それとも以前のバージョンとは異なるのかを自問しています
Lino 2010

8
@Lino下の偉大な答えの最後の引用のための文言は同じにすべての方法のバックでのJava 6:「オペランドの一つが型である場合T Tがあるbyteshortまたはchar、その他のオペランドが定数式ですタイプintその値型で表現可能であり、Tは、その後、条件式のタイプは、T。」---バークレー校でJava1.0ドキュメントを見つけました。同じテキスト。---はい、いつもそうです。
アンドレアス

1
あなたは物事を「見つける」方法が面白いです:Pあなただ歓迎:)
phant0m

回答:


66

まず、ローカル変数についてのみ説明します。事実上、finalはフィールドには適用されません。finalフィールドのセマンティクスは非常に明確であり、コンパイラの最適化とメモリモデルの約束の対象となるため、これは重要です。最終フィールドのセマンティクスについては、17.5.1ドルを参照してください。

表面レベルfinaleffectively finalローカル変数は確かに同じです。ただし、JLSは、このような特殊な状況で実際に幅広い効果をもたらす2つを明確に区別します。


前提

変数についてのJLS§4.12.4からfinal

定数変数があるfinalの変数プリミティブ型または型の文字列で初期化される定数式§15.29)。変数が定数変数であるかどうかは、クラスの初期化(§12.4.1)、バイナリ互換性(§13.1)、到達可能性(§14.22)、および明確な割り当て(§16.1.1)に関して影響与える可能性があります。

ためintのプリミティブであり、変数はaそのようなある一定の変数

さらに、同じ章からeffectively final

最終として宣言されていない特定の変数は、代わりに事実上最終と見なされます。

だから、これは言葉で表現されている方法から、他の例では、ことは明らかであるaされていないことがあるとして、一定の変数と考え、最終的ではないが、唯一効果的に、最終的な。


動作

区別ができたので、何が起こっているのか、なぜ出力が異なるのかを調べてみましょう。

? :ここでは条件演算子を使用しているので、その定義を確認する必要があります。JLS§15.25から:

条件式には、ブール条件式数値条件式参照条件式の3種類があり、第2オペランド式と第3オペランド式で分類されます

この場合、私たちは話している数値の条件式から、JLS§15.25.2

数値条件式のタイプは、次のように決定されます。

そして、それは2つのケースが異なって分類される部分です。

事実上最終

effectively final一致するバージョンは、次のルールに一致します。

それ以外の場合、一般的な数値昇格§5.6)が2番目と3番目のオペランドに適用され、条件式の型は2番目と3番目のオペランドの昇格された型になります。

これは、実行する場合と同じ動作です5 + 'd'。つまりint + char、結果はint。になります。JLS§5.6を参照

数値昇格は、数値コンテキスト内のすべての式の昇格されたタイプを決定します。プロモート型は、各式をプロモート型に変換できるように選択され、算術演算の場合は、プロモート型の値に対して演算が定義されます。数値コンテキストでの式の順序は、数値の昇格には重要ではありません。ルールは次のとおりです。

[...]

次に、次のルールに従って、拡大プリミティブ変換§5.1.2)と縮小プリミティブ変換§5.1.3)が一部の式に適用されます。

数値選択のコンテキストでは、次のルールが適用されます。

いずれかの式が型でintあり、定数式でない場合§15.29)、プロモートされた型はintであり、型ではない他の式は、への拡張プリミティブ変換int受けます。int

だから、すべてがに昇格されintaいるint既に。これは、の出力を説明しています97

最後の

final変数のあるバージョンは、次のルールに一致します。

オペランドの一方がタイプである場合はTここTでありbyteshortまたはchar、その他のオペランドは定数式§15.29タイプの)int値タイプで表現されT、その後、条件式のタイプがありますT

最後の変数aは型でintあり、定数式です(であるためfinal)。として表すことができるcharため、結果はタイプになりcharます。これで出力は終了aです。


文字列の例

文字列が等しい例は同じコアの違いに基づいており、final変数は定数式/変数として扱われ、そうでeffectively finalはありません。

Javaでは、文字列のインターンは定数式に基づいているため、

"a" + "b" + "c" == "abc"

であるtrue(実際のコードでこの構文を使用してはいけない)だけでなく。

JLS§3.10.5を参照してください:

さらに、文字列リテラルは常にクラスStringの同じインスタンスを参照します。これは、文字列リテラル(またはより一般的は定数式値である文字列(§15.29))が、メソッド(§12.5)を使用して一意のインスタンスを共有するように「インターン」されるためです。String.intern

主にリテラルについて話しているので見落としがちですが、実際には定数式にも当てはまります。


8
問題は、あなたが期待するということである... ? a : 'c'かどうかを同じように動作することa、変数定数を。どうやら表現に問題はありません。---これとは対照的に、a + "b" == "ab"ある悪い表現、文字列のニーズを使用して比較されるので、equals()私はJavaで文字列を比較する方法を教えてください)。a定数の場合に「誤って」機能するという事実は、文字列リテラルのインターンの癖にすぎません。
アンドレアス

5
@Andreasはい。ただし、文字列のインターンはJavaの明確に定義された機能であることに注意してください。明日または別のJVMで変更される可能性があるのは偶然ではありません。有効なJava実装である"a" + "b" + "c" == "abc"必要がありますtrue
Zabuzard

10
確かに、それa + "b" == "ab"は明確に定義された癖ですが、それでも間違った表現です。あなたが場合でも知っていることはaある定数、それはコールしないために、あまりにもエラーを起こしやすいですequals()。あるいは、壊れやすいというのが良い言葉かもしれません。つまり、コードが将来維持されるときに崩壊する可能性が高すぎます。
アンドレアス

2
事実上最終変数のプライマリドメイン、つまりラムダ式での使用でも、違いによって実行時の動作が変わる可能性があることに注意してください。つまり、キャプチャと非キャプチャのラムダ式の違いが生じる可能性があり、後者はシングルトンに評価されます。 、しかし前者は新しいオブジェクトを生成します。言い換えると、が(ではない)の(final) String str = "a"; Stream.of(null, null). <Runnable>map( x -> () -> System.out.println(str)) .reduce((a,b) -> () -> System.out.println(a == b)) .ifPresent(Runnable::run);ときに結果を変更します。strfinal
ホルガー

7

もう1つの側面は、変数がメソッドの本体でfinalとして宣言されている場合、パラメーターとして渡されるfinal変数とは異なる動作をすることです。

public void testFinalParameters(final String a, final String b) {
  System.out.println(a + b == "ab");
}

...
testFinalParameters("a", "b"); // Prints false

一方

public void testFinalVariable() {
   final String a = "a";
   final String b = "b";
   System.out.println(a + b == "ab");  // Prints true
}

...
testFinalVariable();

コンパイラが使用していることを知っているので、それが起こるの変数は常にありますように、値をし、問題なく交換することができます。場合と異なり、定義されていないか、それが定義されている(最終である場合、上記の例のようにするが、その値は、実行時に割り当てられたパラメータ)コンパイラは、その使用前には何も知りません。したがって、連結は実行時に発生し、インターンプールを使用せずに新しい文字列が生成されます。final String a = "a"a"a"a"a"afinalfinala


基本的に、動作は次のとおりです。コンパイラが変数が定数であることを知っている場合、定数を使用するのと同じように使用できます。

変数がfinalで定義されていない場合(またはfinalであるが実行時に値が定義されている場合)、値が定数に等しく、値が変更されない場合でも、コンパイラーが変数を定数として処理する理由はありません。


4
その中に奇妙なことは何もありません:)
dbl 2010

2
それは質問の別の側面です。
Davide LorenzoMARINO20年

5
finelパラメータに適用されたキーワードfinal、ローカル変数に適用されたものとは異なるセマンティクスなど...
dbl 2010

6
ここでは、パラメーターの使用は不必要な難読化です。あなたはただやっfinal String a; a = "a";て同じ行動をとることができます
yawkat 2010
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.