在Java中修改最终字段
我们从一个简单的testing用例开始:
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()); } }
任何人都在猜测会输出什么(底部显示为不会立即破坏意外)。
问题是:
- 为什么原始的和包装的整数行为不同?
- 为什么reflection与直接访问返回不同的结果?
- 最让我困扰的是 – 为什么String的行为像原始的
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定义了一个常量expression式,13.4.9讨论了二进制兼容性或final字段和常量。
如果您将字段设置为非最终值或分配一个非编译时间常量,则该值不会内联。 例如:
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
配合FieldAccessor
。
对于int
它得到一个UnsafeQualifiedIntegerFieldAccessorImpl
,它的超类定义readOnly
属性为true,只有当该字段是static
和final
因此,首先回答未经询问的问题 – 这就是为什么final
变更无一例外。
UnsafeQualifiedFieldAccessor
所有子类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
是通过reflection实例化 – 否则不允许)(我调用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 :
如果最终字段在字段声明中被初始化为编译时常量,则可能不会观察到对最终字段的更改,因为在编译时将该最终字段的使用replace为编译时常量。
第9章讨论在这个问题中观察到的细节。
事实certificate,这种行为并不意外,因为final
字段的修改应该在对象初始化之后才发生。
这不是一个答案,但它带来了另一个混乱点:
我想看看这个问题是编译时评估还是反思是否真的允许Java绕过final
关键字。 这是一个testing程序。 我所添加的是另一组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
为什么heck直接调用getWrappedInt()更改?
这有一个工作。 如果你设置私有静态最终在静态{}块中的值,它将工作,因为它不会内联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");