通过反射修改私有 final 字段

55
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" 字段没有被改变,正如我所期望的。但是如果我们通过反射获取该字段,第二个打印显示它已经被更新了。

请查看此链接:https://dev59.com/TXA75IYBdhLWcg3wbofM#31268945 - iirekm
https://dev59.com/TXA75IYBdhLWcg3wbofM#31268945 - iirekm
4个回答

72

这个答案对这个主题进行了详尽的阐述。

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 static 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!

希望这能有点帮助。

1
+1 - 这才是真正的答案。OP的程序导致JIT编译器假设s不会改变,这是无效的。这不是编译器的错误,因为JLS明确警告如果在设置后修改final变量,就会发生类似这样的“坏事情”。 - Stephen C
5
@Stephen C: 实际上,这种行为与 JIT 编译无关。已经是字节码编译器在将 return "s = " + s; 中的 s 替换为编译时常量。可以通过创建两个类 AB 来证明这一点,其中 A 引用 B 中定义的常量。现在,更改 B 中的常量并仅重新编译 B 将保留 A 中的旧常量!有些难以察觉,但确实如此。 - Joonas Pulakka
1
@Stephen C:确实。在我看来,如果JIT编译器能够代替字节码编译器执行这项任务,那将会更好。在当前的行为中,你只需修改类中某个标志常量的,就可以破坏其他人的类。 - Joonas Pulakka
2
我认为你不必恢复字段的可访问性,因为它实际上是某个Java内部“Field”实例的副本。这样的副本只是供您使用的。因此,如果您在代码中以后不需要读取原始的“accessible”标志状态,则不必保留并恢复其状态。 - Jiri Patera
@Tarik:你能具体一点吗?如果这是由于编译时错误“非静态变量this无法从静态上下文中引用”,我已经修复了代码,使其能够在JDK17下编译通过。 - undefined
显示剩余3条评论

18

这个

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";
    }  
}

也就是说,编译时常量会被内联。请参见这个问题。避免内联的最简单方法是像这样声明String

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

对于其他类型,一个简单的方法调用就可以解决问题:

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

1
我真的很喜欢这个清晰的解释和 intern() 方法,以前不知道...谢谢。 - Jiri Patera
3
实际上,最好使用 toString 方法,因为它避免了在字符串池中进行查找。 - Rafael Winterhalter

7
这是一个对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 = " 和私有的 final 字段 " I’m totally safe" 拼接在一起并存储了下来。无论实例变量 s 如何更改,toString() 方法始终会返回 "s = I’m totally safe"

1

由于使用了 final 关键字,编译器期望值不会改变,因此可能已经将字符串硬编码到您的 toString 方法中。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接