Java 5 - “最终”不再是最终的
来自挪威 Machina Networks 的 Narve Saetre 昨天给我发了条消息,提到很遗憾我们可以将手柄更改为最终阵列。我误解了他的意思,开始耐心地解释说我们不能让数组成为常量,而且没有办法保护数组的内容。“不,”他说,“我们可以使用反射来更改最终句柄。”
我尝试了 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;
}
}
JDK 1.1.x
在 JDK 1.1.x 中,我们无法使用反射访问私有字段。但是,我们可以创建另一个具有公共字段的 Person,然后针对它编译我们的类,并交换 Person 类。如果我们针对与我们编译的类不同的类运行,则在运行时没有访问检查。但是,我们无法在运行时使用类交换或反射重新绑定最终字段。
java.lang.reflect.Field 的 JDK 1.1.8 JavaDocs 有以下说法:
- 如果此 Field 对象强制执行 Java 语言访问控制,并且基础字段不可访问,则该方法将引发 IllegalAccessException。
- 如果基础字段是 final,则该方法将引发 IllegalAccessException。
JDK 1.2.x
在 JDK 1.2.x 中,这发生了一些变化。我们现在可以使用 setAccessible(true) 方法使私有字段可访问。现在在运行时检查字段的访问,因此我们不能使用类交换技巧来访问私有字段。但是,我们现在可以突然重新绑定 final 字段!看看这段代码:
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
声明时基元的最终字段,如果类型是基元或字符串,则值是内联的。
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:字段在 java.lang.reflect.Field.set(Native Method) at FinalFieldChange.change(FinalFieldChange.java:8) 在 FinalFieldChange.main(FinalFieldChange.爪哇:12)
java 版本 "1.4.2_05" 异常线程 "main" IllegalAccessException: 字段在 java.lang.reflect.Field.set(Field.java:519) 在 FinalFieldChange.change(FinalFieldChange.java:8) 在 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。然而,我们都觉得这不太可能,尤其是这样一个根本性的错误。经过一番搜索,我找到了 JSR-133:Java 内存模型和线程规范。大部分规范都很难阅读,让我想起了我的大学时代(我曾经这样写;-)但是,JSR-133 非常重要,以至于所有 Java 程序员都应该阅读它。(祝你好运)
从第 25 页的第 9 章最终字段语义开始。具体而言,请阅读第 9.1.1 节最终字段的构造后修改。允许更新最终字段是有意义的。例如,我们可以放宽 JDO 中非 final 字段的要求。
如果我们仔细阅读第 9.1.1 节,我们会发现我们应该只修改 final 字段作为我们构建过程的一部分。用例是我们反序列化一个对象,然后一旦我们构建了对象,我们在传递它之前初始化最终字段。一旦我们使对象对另一个线程可用,我们不应该使用反射来更改最终字段。结果将无法预测。
它甚至这样说:如果在字段声明中将 final 字段初始化为编译时常量,则可能不会观察到对 final 字段的更改,因为该 final 字段的使用在编译时被编译时常量替换。这就解释了为什么我们的 iq 字段保持不变,但国家/地区发生了变化。
奇怪的是,JDK 5 与 JDK 1.2.x 略有不同,因为您不能修改静态 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) 专家时事通讯的几个月前发布!我认为可以肯定地说,很少有 Java 开发人员能记住 JDK1.3.0 之前的细节。啊,怀旧不是以前的样子了!您还记得第一次运行 Java 并收到此错误:“无法初始化线程:找不到类 java/lang/Thread”吗?