まず、パズル:次のコードは何を出力しますか?
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;
}
}
回答:
下のネタバレ。
X
をscale(long)で印刷し、X = scale(10) + 3
を再定義すると、印刷結果はX = 0
、次にX = 3
となります。つまり、X
は一時的に0
に設定され、後で3
に設定されます。これはfinal
の違反です。
静的修飾子は、最終修飾子と組み合わせて、定数の定義にも使用されます。最後の修飾子は、 このフィールドの値は に変更できないことを示します。
ソース: https://docs.Oracle.com/javase/tutorial/Java/javaOO/classvars.html [強調表示]
私の質問:これはバグですか? final
は正しく定義されていますか?
これが私が興味を持っているコードです。X
には2つの異なる値が割り当てられています:0
と3
。これは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の静的最終フィールドの初期化順序 の複製の可能性として報告されています。私の質問ではfinal
タグと組み合わせた周期的な初期化について説明していますが、他の質問では初期化の順序について説明しているので、この質問は not 重複であると思います。他の質問だけでは、自分の質問のコードがエラーにならないのは理解できません。
これは、ernestoが取得する出力を見ると特に明らかです。a
がfinal
でタグ付けされている場合、彼は次の出力を取得します。
a=5
a=5
これは私の質問の主要部分を含まない:final
変数はどのようにその変数を変えるのか?
ここで決勝戦とは関係ありません。
インスタンスまたはクラスレベルなので、まだ何も割り当てられていない場合はデフォルト値が保持されます。代入せずにアクセスしたときに0
が表示されるのはそのためです。
完全に代入しないでX
にアクセスすると、デフォルト値のlongのデフォルト値の0
が保持されます。
バグではありません。
scale
への最初の呼び出しがから呼び出されたとき
private static final long X = scale(10);
return X * value
を評価しようとします。 X
にはまだ値が割り当てられていないため、long
のデフォルト値(0
)が使用されます。
そのため、そのコード行はX * 10
、つまり0 * 10
、つまり0
と評価されます。
それはまったくバグではありません、単純にそれが 違法な形式ではない 前方参照の/ /他には何もないということです。
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
を取得することを可能にします。繰り返しになりますが、これはSpecによって許可されているため(厳密には禁止されていません)、正常に機能します。
クラスレベルメンバは、クラス定義内のコードで初期化できます。コンパイルされたバイトコードはクラスメンバをインラインで初期化できません。 (インスタンスメンバーは同様に扱われますが、これは提供された質問には関係ありません。)
次のように書くと:
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));
}
}
scale(10)
を呼び出してstatic final
フィールドにX
を割り当てます。scale(long)
関数は、初期化されていないX
の値(デフォルトはlongまたは0)を読み取って、クラスが部分的に初期化されている間に実行されます。0 * 10
の値がX
に割り当てられ、クラスローダーが完了します。scale(5)
を呼び出すpublic static void mainメソッドを実行します。このメソッドは、初期化された0のX
値に5を掛けて0を返します。静的最終フィールドX
は一度だけ割り当てられ、final
キーワードが保持する保証は保持されます。代入に3を追加する後続のクエリでは、上記のステップ5が値0 * 10 + 3
である3
の評価になり、mainメソッドは値3 * 5
である15
の結果を出力します。
オブジェクトの初期化されていないフィールドを読むと、コンパイルエラーになるはずです。 Javaにとっては残念ながら、そうではありません。
私が標準の詳細を知らないけれども、これがそうである根本的な理由がオブジェクトがどのように具体化されそして構築されるかの定義の奥深くに「隠されている」と思う。
ある意味では、finalは、その目的がこの問題によるものであることを達成することすらできないため、定義が明確ではありません。しかし、すべてのクラスが正しく書かれていれば、この問題はありません。つまり、すべてのフィールドは常にすべてのコンストラクタに設定され、そのコンストラクタの1つを呼び出さずにオブジェクトが作成されることはありません。シリアライゼーションライブラリを使用する必要があるまでは、これは当然のことです。