Java 5 - 「最終」はもはや最終ではありません
昨日、ノルウェーの Machina Networks の Narve Saetre から、ハンドルを final 配列に変更できるのは残念だというメモが届きました。私は彼を誤解し、配列を定数にすることはできず、配列の内容を保護する方法がないことを辛抱強く説明し始めました。「いいえ」と彼は言いました。「リフレクションを使用して最終ハンドルを変更できます。」
Narve のサンプル コードを試してみたところ、信じられないことに、Java 5 では最終ハンドルを変更できました。プリミティブ フィールドのハンドルも変更できました。ある時点で許可されていたが、その後許可されなくなったことを知っていたので、古いバージョンの Java でいくつかのテストを実行しました。まず、final フィールドを持つクラスが必要です。
public class Person {
private final String name;
private final int age;
private final int iq = 110;
private final Object country = "South Africa";
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String toString() {
return name + ", " + age + " of IQ=" + iq + " from " + country;
}
}
JDK1.1.x
JDK 1.1.x では、リフレクションを使用してプライベート フィールドにアクセスできませんでした。ただし、パブリック フィールドを持つ別の Person を作成し、それに対してクラスをコンパイルして、Person クラスを交換することはできます。コンパイルしたクラスとは異なるクラスに対して実行している場合、実行時にアクセス チェックは行われませんでした。ただし、クラス スワッピングまたはリフレクションを使用して、実行時に final フィールドを再バインドできませんでした。
java.lang.reflect.Field の JDK 1.1.8 JavaDocs には、次のように記載されていました。
- この Field オブジェクトが Java 言語アクセス制御を適用し、基になるフィールドにアクセスできない場合、メソッドは IllegalAccessException をスローします。
- 基になるフィールドが final の場合、メソッドは IllegalAccessException をスローします。
JDK1.2.x
JDK 1.2.x では、これが少し変更されました。setAccessible(true) メソッドでプライベート フィールドにアクセスできるようになりました。フィールドへのアクセスは実行時にチェックされるようになったため、クラス スワッピング トリックを使用してプライベート フィールドにアクセスすることはできませんでした。ただし、最終フィールドを突然再バインドできるようになりました。このコードを見てください:
import java.lang.reflect.Field;
public class FinalFieldChange {
private static void change(Person p, String name, Object value)
throws NoSuchFieldException, IllegalAccessException {
Field firstNameField = Person.class.getDeclaredField(name);
firstNameField.setAccessible(true);
firstNameField.set(p, value);
}
public static void main(String[] args) throws Exception {
Person heinz = new Person("Heinz Kabutz", 32);
change(heinz, "name", "Ng Keng Yap");
change(heinz, "age", new Integer(27));
change(heinz, "iq", new Integer(150));
change(heinz, "country", "Malaysia");
System.out.println(heinz);
}
}
これを JDK 1.2.2_014 で実行すると、次の結果が得られました。
Ng Keng Yap, 27 of IQ=110 from Malaysia Note, no exceptions, no complaints, and an incorrect IQ result. It seems that if we set a
宣言時のプリミティブの final フィールド。型がプリミティブまたは文字列の場合、値はインライン化されます。
JDK 1.3.x および 1.4.x
JDK 1.3.x では、Sun はアクセスを少し強化し、リフレクションで final フィールドを変更できないようにしました。これは JDK 1.4.x でも同様でした。FinalFieldChange クラスを実行して、リフレクションを使用して実行時に最終フィールドを再バインドしようとすると、次のようになります。
java バージョン "1.3.1_12": 例外スレッド "main" IllegalAccessException: フィールドは、FinalFieldChange.main(FinalFieldChange.ジャワ:12)
Java バージョン "1.4.2_05" 例外スレッド "main" IllegalAccessException: フィールドは FinalFieldChange.main(FinalFieldChange.java:8) で java.lang.reflect.Field.set(Field.java:519) で最終ですFinalFieldChange.java:12)
JDK5.x
これで、JDK 5.x に到達します。FinalFieldChange クラスには、JDK 1.2.x と同じ出力があります。
Ng Keng Yap, 27 of IQ=110 from Malaysia When Narve Saetre mailed me that he managed to change a final field in JDK 5 using
振り返ってみると、バグがJDKに忍び込んだことを望んでいました。ただし、特にそのような根本的なバグはありそうにないと感じました。いくつか検索した結果、JSR-133: Java Memory Model and Thread Specification を見つけました。仕様の大部分は読みにくいので、大学時代を思い出します (以前はそのように書いていました ;-) しかし、JSR-133 は非常に重要であるため、すべての Java プログラマーが必ず読む必要があります。(幸運を)
25 ページの第 9 章「最終フィールド セマンティクス」から始めます。具体的には、セクション 9.1.1 最終フィールドの構築後の変更をお読みください。final フィールドの更新を許可することは理にかなっています。たとえば、JDO で非 final フィールドを持つという要件を緩和できます。
セクション 9.1.1 を注意深く読むと、構築プロセスの一部として final フィールドのみを変更する必要があることがわかります。ユースケースは、オブジェクトをデシリアライズし、オブジェクトを構築したら、それを渡す前に最終フィールドを初期化する場合です。オブジェクトを別のスレッドで使用できるようにしたら、リフレクションを使用して final フィールドを変更しないでください。結果は予測できません。
それは次のようにも言っています: final フィールドがフィールド宣言でコンパイル時の定数に初期化されている場合、その final フィールドの使用はコンパイル時にコンパイル時の定数に置き換えられるため、final フィールドへの変更は観察されない可能性があります。これは、iq フィールドが同じままであるのに、国が変わる理由を説明しています。
奇妙なことに、JDK 5 は JDK 1.2.x とは少し異なり、 static final フィールドを変更することはできません。
import java.lang.reflect.Field;
public class FinalStaticFieldChange {
/** Static fields of type String or primitive would get inlined */
private static final String stringValue = "original value";
private static final Object objValue = stringValue;
private static void changeStaticField(String name)
throws NoSuchFieldException, IllegalAccessException {
Field statFinField = FinalStaticFieldChange.class.getDeclaredField(name);
statFinField.setAccessible(true);
statFinField.set(null, "new Value");
}
public static void main(String[] args) throws Exception {
changeStaticField("stringValue");
changeStaticField("objValue");
System.out.println("stringValue = " + stringValue);
System.out.println("objValue = " + objValue);
System.out.println();
}
}
これを JDK 1.2.x および JDK 5.x で実行すると、次の出力が得られます。
Java バージョン "1.2.2_014": stringValue = 元の値 objValue = 新しい値
java バージョン "1.5.0" 例外スレッド "main" IllegalAccessException: Field is final at java.lang.reflect.Field.set(Field.java:656) at FinalStaticFieldChange.changeStaticField(12) at FinalStaticFieldChange.main(16)
では、JDK 5 は JDK 1.2.x に似ていて、違うだけですか?
結論
JDK 1.3.0 がいつリリースされたか知っていますか? 調べるのに苦労したので、ダウンロードしてインストールしました。readme.txt ファイルの日付は 2000/06/02 13:10 です。だから、それは4歳以上です(昨日のように感じます). JDK 1.3.0 は、私が Java(tm) Specialists' Newsletter を書き始める数か月前にリリースされました。JDK1.3.0 より前の詳細を覚えている Java 開発者はほとんどいないと言っても過言ではありません。ああ、懐かしさは以前のものではありません!初めて Java を実行したときに、「スレッドを初期化できません: クラス java/lang/Thread が見つかりません」というエラーが発生したことを覚えていますか?