让我们从一个简单的测试用例开始:
import java.lang.reflect.Field;
public class Test {
private final int primitiveInt = 42;
private final Integer wrappedInt = 42;
private final String stringValue = "42";
public int getPrimitiveInt() { return this.primitiveInt; }
public int getWrappedInt() { return this.wrappedInt; }
public String getStringValue() { return this.stringValue; }
public void changeField(String name, Object value) throws IllegalAccessException, NoSuchFieldException {
Field field = Test.class.getDeclaredField(name);
field.setAccessible(true);
field.set(this, value);
System.out.println("reflection: " + name + " = " + field.get(this));
}
public static void main(String[] args) throws IllegalAccessException, NoSuchFieldException {
Test test = new Test();
test.changeField("primitiveInt", 84);
System.out.println("direct: primitiveInt = " + test.getPrimitiveInt());
test.changeField("wrappedInt", 84);
System.out.println("direct: wrappedInt = " + test.getWrappedInt());
test.changeField("stringValue", "84");
System.out.println("direct: stringValue = " + test.getStringValue());
}
}
任何人都想猜测将打印什么作为输出(显示在底部,以免立即破坏惊喜)。
问题是:
int
而不是像 Integer
?结果(java 1.5):
reflection: primitiveInt = 84
direct: primitiveInt = 42
reflection: wrappedInt = 84
direct: wrappedInt = 84
reflection: stringValue = 84
direct: stringValue = 42
编译时常量是内联的(在 javac 编译时)。请参阅 JLS,特别是 15.28 定义了常量表达式,13.4.9 讨论了二进制兼容性或最终字段和常量。
如果将字段设置为非最终字段或分配非编译时间常量,则该值不会内联。例如:
private final String stringValue = null!=null?"": "42";
在我看来,这更糟糕:一位同事指出了以下有趣的事情:
@Test public void testInteger() throws SecurityException, NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
Field value = Integer.class.getDeclaredField("value");
value.setAccessible(true);
Integer manipulatedInt = Integer.valueOf(7);
value.setInt(manipulatedInt, 666);
Integer testInt = Integer.valueOf(7);
System.out.println(testInt.toString());
}
通过这样做,您可以更改正在运行的整个 JVM 的行为。 (当然你可以只改变-127到127之间的值)
Reflection 的
set(..)
方法与 FieldAccessor
s 一起使用。
对于
int
,它会得到一个 UnsafeQualifiedIntegerFieldAccessorImpl
,其超类将 readOnly
属性定义为 true,仅当字段为 static
和 final
时
因此,首先回答未提出的问题 - 这就是为什么
final
无一例外地更改的原因。
UnsafeQualifiedFieldAccessor
的所有子类都使用sun.misc.Unsafe
类来获取值。那里的方法都是native
,但它们的名字是getVolatileInt(..)
和getInt(..)
(分别为getVolatileObject(..)
和getObject(..)
)。上述访问器使用“易失性”版本。如果我们添加非易失性版本,会发生以下情况:
System.out.println("reflection: non-volatile primitiveInt = "
unsafe.getInt(test, (long) unsafe.fieldOffset(getField("primitiveInt"))));
(其中
unsafe
通过反射实例化 - 否则不允许)
(我称 getObject
为 Integer
和 String
)
这给出了一些有趣的结果:
reflection: primitiveInt = 84
direct: primitiveInt = 42
reflection: non-volatile primitiveInt = 84
reflection: wrappedInt = 84
direct: wrappedInt = 84
reflection: non-volatile wrappedInt = 84
reflection: stringValue = 84
direct: stringValue = 42
reflection: non-volatile stringValue = 84
此时我想起 javaspecialists.eu 上的一篇文章讨论了相关问题。它引用了JSR-133:
如果在字段声明中将最终字段初始化为编译时常量,则可能不会观察到对最终字段的更改,因为该最终字段的使用在编译时被替换为编译时常量。
第 9 章讨论了在这个问题中观察到的细节。
事实证明这种行为并不那么意外,因为
final
字段的修改应该只在对象初始化后立即发生。
这不是一个答案,但它提出了另一个令人困惑的点:
我想看看问题是否出在编译时评估上,或者反射是否实际上允许 Java 绕过
final
关键字。这是一个测试程序。我添加的只是另一组 getter 调用,因此每个 changeField()
调用之前和之后都有一个。
package com.example.gotchas;
import java.lang.reflect.Field;
public class MostlyFinal {
private final int primitiveInt = 42;
private final Integer wrappedInt = 42;
private final String stringValue = "42";
public int getPrimitiveInt() { return this.primitiveInt; }
public int getWrappedInt() { return this.wrappedInt; }
public String getStringValue() { return this.stringValue; }
public void changeField(String name, Object value) throws IllegalAccessException, NoSuchFieldException {
Field field = MostlyFinal.class.getDeclaredField(name);
field.setAccessible(true);
field.set(this, value);
System.out.println("reflection: " + name + " = " + field.get(this));
}
public static void main(String[] args) throws IllegalAccessException, NoSuchFieldException {
MostlyFinal test = new MostlyFinal();
System.out.println("direct: primitiveInt = " + test.getPrimitiveInt());
test.changeField("primitiveInt", 84);
System.out.println("direct: primitiveInt = " + test.getPrimitiveInt());
System.out.println();
System.out.println("direct: wrappedInt = " + test.getWrappedInt());
test.changeField("wrappedInt", 84);
System.out.println("direct: wrappedInt = " + test.getWrappedInt());
System.out.println();
System.out.println("direct: stringValue = " + test.getStringValue());
test.changeField("stringValue", "84");
System.out.println("direct: stringValue = " + test.getStringValue());
}
}
这是我得到的输出(在 Eclipse、Java 1.6 下)
direct: primitiveInt = 42
reflection: primitiveInt = 84
direct: primitiveInt = 42
direct: wrappedInt = 42
reflection: wrappedInt = 84
direct: wrappedInt = 84
direct: stringValue = 42
reflection: stringValue = 84
direct: stringValue = 42
为什么直接调用 getWrappedInt() 会发生变化?
有一个解决办法。如果您在 static {} 块中设置私有静态最终归档的值,它将起作用,因为它不会内联 fileld:
private static final String MY_FIELD;
static {
MY_FIELD = "SomeText"
}
...
Field field = VisitorId.class.getDeclaredField("MY_FIELD");
field.setAccessible(true);
field.set(field, "fakeText");