最終的な定義は不適切ですか?


186

まず、パズル:次のコードは何を出力しますか?

public class RecursiveStatic {
    public static void main(String[] args) {
        System.out.println(scale(5));
    }

    private static final long X = scale(10);

    private static long scale(long value) {
        return X * value;
    }
}

回答:

0

下のネタバレ。


Xscale(long)で印刷してを再定義するX = scale(10) + 3と、印刷はにX = 0なりX = 3ます。つまり、Xは一時的にに設定され0、後でに設定され3ます。これは違反ですfinal

static修飾子は、final修飾子と組み合わせて、定数の定義にも使用されます。最後の修飾子は、このフィールドの値を変更できないことを示しています。

ソース:https : //docs.oracle.com/javase/tutorial/java/javaOO/classvars.html [強調を追加]


私の質問:これはバグですか?されfinal不明確な?


これが私が興味を持っているコードです X。2つの異なる値が割り当てられています:03。これはの違反だと思いますfinal

public class RecursiveStatic {
    public static void main(String[] args) {
        System.out.println(scale(5));
    }

    private static final long X = scale(10) + 3;

    private static long scale(long value) {
        System.out.println("X = " + X);
        return X * value;
    }
}

この質問には、Java static finalフィールドの初期化順序の重複の可能性があるというフラグが付けられています。他の質問は初期化の順序を扱っているので、この質問は重複していないと思いますが、私の質問は、finalタグ。他の質問だけでは、なぜ私の質問のコードがエラーにならないのか理解できません。

これは、ernestoが取得する出力を見ると特に明確aです。がタグ付けされているfinal場合、彼は次の出力を取得します。

a=5
a=5

これは私の質問の主要部分に関係していません:final変数はどのように変数を変更しますか?


17
Xメンバーを参照するこの方法は、スーパークラスコンストラクターが完了する前にサブクラスメンバーを参照するようなものですfinal。これは問題であり、の定義ではありません。
daniu

4
JLSから:A blank final instance variable must be definitely assigned (§16.9) at the end of every constructor (§8.8) of the class in which it is declared; otherwise a compile-time error occurs.
Ivan

1
@Ivan、これは定数についてではなく、インスタンス変数についてです。しかし、チャプターを追加できますか?
AxelH 2018年

9
注意点として、本番用コードではこれを行わないでください。誰かがJLSの抜け穴を悪用し始めると、それは誰にとっても非常に混乱します。
Zabuzard

13
参考までに、これとまったく同じ状況をC#でも作成できます。定数宣言でループするというC#の約束はコンパイル時にキャッチされますが、読み取り専用宣言についてはそのような約束をしません。実際には、フィールドの初期ゼロ値が別のフィールド初期化子によって監視される状況に陥ることがあります。あなたがそれをするときにそれが痛いなら、それをしないでください。コンパイラはあなたを救いません。
エリックリッペルト2018年

回答:


217

非常に興味深い発見。それを理解するには、Java言語仕様(JLS)ます。

その理由は、割り当てをfinal 1つしか許可しないためです。ただし、デフォルト値は割り当てなしです。実際、そのようなすべての変数(クラス変数、インスタンス変数、配列コンポーネント)は、代入の前に、最初からデフォルト値を指しています。次に、最初の割り当てによって参照が変更されます。


クラス変数とデフォルト値

次の例を見てください。

private static Object x;

public static void main(String[] args) {
    System.out.println(x); // Prints 'null'
}

x指すが、明示的に値を割り当てなかったnullいます、それはデフォルト値です。それを§4.12.5と比較してください

変数の初期値

クラス変数、インスタンス変数、または配列コンポーネントは、作成時にデフォルト値で初期化されます§15.9 §15.10.2

これは、この例のように、これらの種類の変数にのみ当てはまることに注意してください。ローカル変数は保持されません。次の例を参照してください。

public static void main(String[] args) {
    Object x;
    System.out.println(x);
    // Compile-time error:
    // variable x might not have been initialized
}

同じJLS段落から:

ローカル変数14.4§14.14は)されなければならない値を明示的に与えられ、それが初期化(いずれかによって、使用される前に14.4)又は割り当て(§15.26(明確な割り当ての規則を使用して検証することができるように、)§ 16(明確な割り当て))。


最終変数

§4.12.4finalから、を見てみましょ

最終的な変数

変数はfinalと宣言できます。最終的な変数のみをすることができる一度に割り当てます最終変数が割り当てられる直前に確実に割り当て解除されない限り(§16(明確な割り当て))、最終変数が割り当てられていると、コンパイル時エラーになります。


説明

ここで、少し変更した例に戻ります。

public static void main(String[] args) {
    System.out.println("After: " + X);
}

private static final long X = assign();

private static long assign() {
    // Access the value before first assignment
    System.out.println("Before: " + X);

    return X + 1;
}

出力します

Before: 0
After: 1

私たちが学んだことを思い出してください。メソッド内でassign、変数にXはまだ値が割り当てられていません。したがって、それはクラス変数であり、JLSに従ってこれらの変数は常に(ローカル変数とは対照的に)デフォルト値を指すため、デフォルト値を指します。assignメソッドの後、変数にX値が割り当てられます。これは、変更できない1ためですfinal。したがって、次の理由により機能しませんfinal

private static long assign() {
    // Assign X
    X = 1;

    // Second assign after method will crash
    return X + 1;
}

JLSでの例

@Andrewのおかげで、このシナリオを正確にカバーするJLS段落を見つけました。また、それを示しています。

しかし、最初に見てみましょう

private static final long X = X + 1;
// Compile-time error:
// self-reference in initializer

メソッドからのアクセスが許可されているのに、なぜこれが許可されないのですか?見てみましょう§8.3.3フィールドがまだ初期化されていない場合に、フィールドへのアクセスが制限される時期について説明を。

クラス変数に関連するいくつかのルールをリストします:

fクラスまたはインターフェイスCで宣言されたクラス変数への単純な名前による参照の場合、次の場合はコンパイル時エラーになります

  • 参照CC8.7)のクラス変数初期化子または静的初期化子のいずれかに表示されます。そして

  • 参照は、fの独自の宣言子の初期化子、またはfの宣言子の左側のポイントに表示されます。そして

  • 参照は代入式の左側にはありません(§15.26)。そして

  • 参照を囲む最も内側のクラスまたはインターフェースはCです。

それX = X + 1は簡単で、はそれらのルールに捕らえられ、メソッドアクセスはそうではありません。彼らはこのシナリオを挙げ、例を挙げています:

メソッドによるアクセスはこの方法ではチェックされないため、次のようになります。

class Z {
    static int peek() { return j; }
    static int i = peek();
    static int j = 1;
}
class Test {
    public static void main(String[] args) {
        System.out.println(Z.i);
    }
}

出力を生成します:

0

変数初期化子iは、クラスメソッドpeekを使用して、変数初期化子によって初期化されるj前の変数の値にアクセスするためj、その時点ではまだデフォルト値4.14.12.5)があるためです。


1
@Andrewはい、クラス変数、ありがとう。そうです、そのようなアクセスを制限するいくつかの特別なルールがなければ、それうまくいきます:§8.3.3クラス変数に指定された4つのポイント(最初のエントリ)を見てください。OPの例でのメソッドアプローチは、これらのルールによって捕捉されないためX、メソッドからアクセスできます。私はそんなに気にしないでしょう。それは、JLSが詳細に機能するものをどのように正確に定義するかに依存します。私はそのようなコードを決して使用しません。それはJLSのいくつかのルールを悪用しているだけです。
ザブザード

4
問題は、コンストラクターからインスタンスメソッドを呼び出すことができることです。これはおそらく許可されるべきではありませんでした。一方、superを呼び出す前にローカルを割り当てることは、便利で安全ですが、許可されていません。図を行きます。
モニカを

1
@Andrewあなたはおそらく、ここで実際に言及されている唯一の人ですforwards references(JLSの一部でもあります)。これは非常に簡単なことで、この悪質な答えはありません。stackoverflow.com/a/49371279/ 1059372
Eugene

1
「その後、最初の割り当ては参照を変更します。」この場合、それは参照型ではなく、プリミティブ型です。
ファビアン

1
この答えは、少し長ければ正しいです。:-)私はtl; drは、OP がJLSではなく「[ファイナル]フィールドは変更できない」というチュートリアルを引用したと思います。Oracleのチュートリアルは非常に優れていますが、すべてのエッジケースを網羅しているわけではありません。OPの質問については、最終的なJLSの実際の定義に移動する必要があります。その定義は、最終フィールドの値が決して変更できないという主張(OPが正当に異議を申し立てる)を行うものではありません。
yshavit 2018年

22

ここではfinalとは関係ありません。

インスタンスレベルまたはクラスレベルであるため、まだ何も割り当てられていない場合はデフォルト値を保持します。これが、0割り当てなしでアクセスしたときに表示される理由です。

X完全に割り当てずにアクセスすると0、longのデフォルト値であるが保持されるため、結果が保持されます。


3
これについてトリッキー
なのは

2
@AxelHどういう意味かわかります。しかし、そうでなければそれはそれが機能するはずです、さもなければ世界は崩壊します;)。
Suresh Atta 2018年

20

バグではありません。

への最初の呼び出しscaleが呼び出されたとき

private static final long X = scale(10);

評価しようreturn X * valueXにはまだ値が割り当てられていないため、aのデフォルト値longが使用されます(0)ます。

コード評価さのラインだから、X * 10すなわち0 * 10です0


8
私はそれがOPを混乱させるものではないと思います。混乱するのはX = scale(10) + 3。なのでX、メソッドから参照すると、です0。しかし、その後です3。したがって、OPはにX2つの異なる値が割り当てられていると考えます。これはと競合しfinalます。
Zabuzard

4
@Zabuzaこれは「と説明されていないことを評価しようとしますreturn X * valueXのデフォルト値をとるので、ない値が割り当てられており、まだとlongしていますか0」?Xデフォルト値が割り当てられているとは言われていませんが、デフォルト値Xによって「置き換えられた」(その用語を引用しないでください;))。
AxelH 2018年

14

それはまったくバグではありません。単に、前方参照の違法な形式ではなく、それ以上のものではありません。

String x = y;
String y = "a"; // this will not compile 


String x = getIt(); // this will compile, but will be null
String y = "a";

public String getIt(){
    return y;
}

これは仕様で単純に許可されています。

あなたの例を挙げると、これはまさにこれが一致する場所です:

private static final long X = scale(10) + 3;

あなたはそれへの前方参照を行っていますがscale、前述のように違法ではありませんが、デフォルト値のを取得できますX。繰り返しますが、これは仕様で許可されています(より正確には禁止されていません)。


いい答えだ!仕様 2番目のケースのコンパイルを許可する理由に興味があります。最終フィールドの「一貫性のない」状態を確認する唯一の方法ですか?
Andrew Tobilko

@Andrewこれはかなり長い間私を悩ませてきました。C++またはCがそれを行うと思う傾向があります(これが本当かどうかはわかりません)
Eugene

@Andrew:そうでなければ、チューリングの不完全性定理を解くことになるからです。
ジョシュア

9
@ジョシュア:ここでは、(1)停止問題、(2)決定問題、(3)Godelの不完全性定理、(4)チューリング完全プログラミング言語など、さまざまな概念を混同していると思います。コンパイラの作成者は、「この変数は使用される前に確実に割り当てられますか?」という問題の解決を試みません。完全にその問題は停止問題を解決することと同等であり、私たちはそうすることができないことを知っているからです。
エリックリッペルト2018年

4
@EricLippert:ははおっと。不完全さをチューリングし、問題を停止することは私の心の同じ場所を占めています。
ジョシュア

4

クラスレベルのメンバーは、クラス定義内のコードで初期化できます。コンパイルされたバイトコードは、クラスメンバーをインラインで初期化できません。(インスタンスメンバーは同様に処理されますが、これは提供された質問には関係ありません。)

次のようなものを書いたとき:

public class Demo1 {
    private static final long DemoLong1 = 1000;
}

生成されるバイトコードは次のようになります。

public class Demo2 {
    private static final long DemoLong2;

    static {
        DemoLong2 = 1000;
    }
}

初期化コードは、クラスローダーが最初にクラスをロードするときに実行される静的初期化子内に配置されます。この知識があれば、元のサンプルは次のようになります。

public class RecursiveStatic {
    private static final long X;

    private static long scale(long value) {
        return X * value;
    }

    static {
        X = scale(10);
    }

    public static void main(String[] args) {
        System.out.println(scale(5));
    }
}
  1. JVMは、jarのエントリポイントとしてRecursiveStaticをロードします。
  2. クラス定義がロードされると、クラスローダーは静的初期化子を実行します。
  3. 初期化子はフィールドscale(10)を割り当てる関数を呼び出しstatic finalますX
  4. scale(long)クラスは、部分的に初期化されていない値を読み取る初期化されている間、機能が実行されますX、長いまたは0のデフォルトであるが。
  5. 0 * 10に割り当てられていますX、クラスローダーが完了します。
  6. JVMはpublic static void mainメソッド呼び出しscale(5)を実行し、初期化されたX値0に5を掛けて0を返します。

static finalフィールドXは1回だけ割り当てられ、finalキーワードが保持する保証が保持されます。割り当てに3を追加する後続のクエリでは、上記のステップ5が0 * 10 + 3which の評価3となり、mainメソッドはvalue の結果を出力3 * 5します15


3

オブジェクトの初期化されていないフィールドを読み取ると、コンパイルエラーが発生するはずです。残念ながら、Javaにはありません。

これがそうである根本的な理由は、オブジェクトのインスタンス化と構築の方法の定義の奥深くに「隠されている」と思いますが、標準の詳細はわかりません。

ある意味では、finalは明確に定義されていません。なぜなら、この問題のために、明記された目的を達成することすらできないからです。ただし、すべてのクラスが適切に記述されていれば、この問題はありません。つまり、すべてのフィールドは常にすべてのコンストラクターで設定され、そのコンストラクターの1つを呼び出さない限りオブジェクトは作成されません。シリアライゼーションライブラリを使用する必要があるまで、これは自然なことです。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.