web-dev-qa-db-ja.com

リフレクションによるプライベート最終フィールドの変更

class WithPrivateFinalField {
    private final String s = "I’m totally safe";
    public String toString() {
        return "s = " + s;
    }
}
WithPrivateFinalField pf = new WithPrivateFinalField();
System.out.println(pf);
Field f = pf.getClass().getDeclaredField("s");
f.setAccessible(true);
System.out.println("f.get(pf): " + f.get(pf));
f.set(pf, "No, you’re not!");
System.out.println(pf);
System.out.println(f.get(pf));

出力:

s = I’m totally safe
f.get(pf): I’m totally safe
s = I’m totally safe
No, you’re not!

なぜこのように機能するのですか、説明してもらえますか?最初の印刷物は、私が期待するように、プライベート「s」フィールドが変更されていないことを示しています。しかし、リフレクションを介してフィールドを取得すると、2番目の印刷が表示され、更新されます。

50
Alexandr

この回答 は、このトピックに関する完全な説明ではありません。

JLS 17.5.3最終フィールドのその後の変更

それでも、多くの合併症があります。最終フィールドがフィールド宣言でコンパイル時定数に初期化されている場合、その最終フィールドの使用はコンパイル時にコンパイル時定数に置き換えられるため、最終フィールドへの変更は観察されない場合があります。

しかし、上記の段落を非常に注意深く読んだ場合、ここで回避策を見つけることができます(private finalフィールドは、フィールド定義ではなくコンストラクタ内にあります):

import Java.lang.reflect.Field;


public class Test {

  public static void main(String[] args) throws Exception {
    WithPrivateFinalField pf = new WithPrivateFinalField();
    System.out.println(pf);
    Field f = pf.getClass().getDeclaredField("s");
    f.setAccessible(true);
    System.out.println("f.get(pf): " + f.get(pf));
    f.set(pf, "No, you’re not!");
    System.out.println(pf);
    System.out.println("f.get(pf): " + f.get(pf));
  }

  private class WithPrivateFinalField {
    private final String s;

    public WithPrivateFinalField() {
      this.s = "I’m totally safe";
    }
    public String toString() {
      return "s = " + s;
    }
  }

}

出力は次のようになります。

s = I’m totally safe
f.get(pf): I’m totally safe
s = No, you’re not!
f.get(pf): No, you’re not!

これが少し役立つことを願っています。

70
Jiri Patera

この

class WithPrivateFinalField {
    private final String s = "I’m totally safe";
    public String toString() {
        return "s = " + s;
    }  
} 

実際に次のようにコンパイルします:

class WithPrivateFinalField {
    private final String s = "I’m totally safe";
    public String toString() {
        return "s = I’m totally safe";
    }  
}

つまり、コンパイル時定数はインライン化されます。this 質問を参照してください。インライン化を回避する最も簡単な方法は、次のようにStringを宣言することです。

private final String s = "I’m totally safe".intern();

他の型については、簡単なメソッド呼び出しがトリックを行います:

private final int integerConstant = identity(42);
private static int identity(int number) {
    return number;
}
15
Joonas Pulakka

以下はWithPrivateFinalFieldクラスファイルの逆コンパイルです(簡単にするために別のクラスに入れています)。

_  WithPrivateFinalField();
     0  aload_0 [this]
     1  invokespecial Java.lang.Object() [13]
     4  aload_0 [this]
     5  ldc <String "I’m totally safe"> [8]
     7  putfield WithPrivateFinalField.s : Java.lang.String [15]
    10  return
      Line numbers:
        [pc: 0, line: 2]
        [pc: 4, line: 3]
        [pc: 10, line: 2]
      Local variable table:
        [pc: 0, pc: 11] local: this index: 0 type: WithPrivateFinalField

  // Method descriptor #22 ()Ljava/lang/String;
  // Stack: 1, Locals: 1
  public Java.lang.String toString();
    0  ldc <String "s = I’m totally safe"> [23]
    2  areturn
      Line numbers:
        [pc: 0, line: 6]
      Local variable table:
        [pc: 0, pc: 3] local: this index: 0 type: WithPrivateFinalField
_

toString()メソッドでは、アドレス0 [_0 ldc <String "s = I’m totally safe"> [23]_]で使用される定数は、コンパイラーが既に文字列リテラル_"s = "_とプライベート最終フィールド_" I’m totally safe"_を連結していることを示しています事前に保管してください。 toString()メソッドは、インスタンス変数sの変更方法に関係なく、常に_"s = I’m totally safe"_を返します。

6
Bert F

finalであるため、コンパイラは値が変更されないことを期待したため、おそらく文字列をtoStringメソッドに直接ハードコーディングしました。

1
Gabe