Java 5 - “最终”不再是最终的
挪威Machina Networks的Narve Saetre昨天给我发了一张纸条,提到我们可以将句柄更改为最终数组,这很遗憾。我误解了他,并开始耐心地解释我们不能使数组恒定,并且没有办法保护数组的内容。“不,”他说,“我们可以用反射来改变最终的句柄。
我尝试了Narve的示例代码,令人难以置信的是,Java 5允许我修改最终句柄,甚至是原始字段的句柄!我知道它曾经在某个时候被允许,但后来被禁止了,所以我用旧版本的Java运行了一些测试。首先,我们需要一个包含最终字段的类:
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;
}
}
JDK 1.1.x
在 JDK 1.1.x 中,我们无法使用反射访问私有字段。但是,我们可以创建另一个具有公共字段的 Person,然后针对该字段编译我们的类,并交换 Person 类。如果我们针对与编译的类不同的类运行,则在运行时没有访问检查。但是,我们无法在运行时使用类交换或反射重新绑定最终字段。
JDK 1.1.8 JavaDocs for java.lang.reflect.Field有以下说法:
- 如果此 Field 对象强制实施 Java 语言访问控制,并且基础字段不可访问,则该方法将引发非法访问异常。
- 如果基础字段是 final,则该方法将引发非法访问异常。
JDK 1.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
在声明时基元的最后一个字段,如果类型是基元或 String,则值是内联的。
JDK 1.3.x 和 1.4.x
在 JDK 1.3.x 中,Sun 稍微收紧了访问,并阻止我们修改带有反射的最终字段。JDK 1.4.x 也是如此。如果我们尝试运行 FinalFieldChange 类,以便在运行时使用反射重新绑定最终字段,我们将得到:
java 版本 “1.3.1_12”: 异常线程 “main” IllegalAccessException: field is final at java.lang.reflect.Field.set(Native Method) at FinalFieldChange.change(FinalFieldChange.java:8) at FinalFieldChange.main(FinalFieldChange.java:12)
java 版本 “1.4.2_05” 异常线程 “main” IllegalAccessException: Field is final at java.lang.reflect.Field.set(Field.java:519) at FinalFieldChange.change(FinalFieldChange.java:8) at FinalFieldChange.main(FinalFieldChange.java:12)
JDK 5.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。但是,我们都觉得这不太可能,尤其是这样一个根本性的bug。经过一番搜索,我找到了JSR-133:Java内存模型和线程规范。大多数规范都是难以阅读的,让我想起了我的大学时代(我曾经这样写过;-)但是,JSR-133 非常重要,因此所有 Java 程序员都应该需要阅读它。(祝你好运)
从第 25 页的第 9 章“最终字段语义”开始。具体而言,请阅读第9.1.1节“最终字段的施工后修改”。允许更新最终字段是有意义的。例如,我们可以放宽在 JDO 中具有非 final 字段的要求。
如果我们仔细阅读第 9.1.1 节,我们会发现,作为构建过程的一部分,我们只应修改最终字段。用例是,我们反序列化一个对象,然后一旦我们构造了该对象,我们就会初始化最终的字段,然后再将其传递。一旦我们将对象提供给另一个线程,我们就不应该使用反射来更改最终字段。结果是无法预测的。
它甚至这样说:如果在字段声明中将最终字段初始化为编译时常量,则可能不会观察到对最终字段的更改,因为在编译时,该最终字段的使用将替换为编译时常量。这就解释了为什么我们的iq字段保持不变,但国家/地区会发生变化。
奇怪的是,JDK 5 与 JDK 1.2.x 略有不同,因为您无法修改静态最终字段。
import java.lang.reflect.Field;
public class FinalStaticFieldChange {
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 = new Value
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 是什么时候发布的吗?我很难找到答案,所以我下载并安装了它。自述文件.txt日期为 2000/06/02 13:10。所以,它已经超过4岁了(天哪,感觉就像昨天一样)。JDK 1.3.0 在我开始编写 Java(tm) 专家通讯之前的几个月就发布了!我认为可以肯定地说,很少有Java开发人员能够记住JDK1.3.0之前的细节。啊,怀旧已今非昔比!您是否还记得第一次运行Java并收到此错误:“无法初始化线程:找不到类java / lang / Thread”?