在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()); } } 

任何人都在猜测会输出什么(底部显示为不会立即破坏意外)。

问题是:

  1. 为什么原始的和包装的整数行为不同?
  2. 为什么reflection与直接访问返回不同的结果?
  3. 最让我困扰的是 – 为什么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,只有当该字段是staticfinal

因此,首先回答未经询问的问题 – 这就是为什么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 IntegerString

这给出了一些有趣的结果:

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