54

让我们从一个简单的测试用例开始:

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. 为什么反射与直接访问会返回不同的结果?
  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
4

5 回答 5

22

编译时常量是内联的(在 javac 编译时)。请参阅 JLS,特别是 15.28 定义了一个常量表达式,并且 13.4.9 讨论了二进制兼容性或最终字段和常量。

如果您使该字段成为非最终字段或分配一个非编译时间常数,则该值不会被内联。例如:

private final String stringValue = null!=null?"": "42";

于 2009-10-23T18:39:23.277 回答
10

在我看来,情况更糟:一位同事指出了以下有趣的事情:

@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 之间的值)

于 2011-02-08T08:48:45.673 回答
8

Reflection 的set(..)方法适用于FieldAccessors。

因为int它得到一个UnsafeQualifiedIntegerFieldAccessorImpl,它的超类将readOnly属性定义为只有当字段是两者 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要求Integerand 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

如果在字段声明中将 final 字段初始化为编译时常量,则可能不会观察到对 final 字段的更改,因为该 final 字段的使用在编译时被编译时常量替换。

第 9 章讨论了在这个问题中观察到的细节。

事实证明,这种行为并没有那么出乎意料,因为final字段的修改应该只在对象初始化之后才发生。

于 2010-01-21T09:02:25.630 回答
1

这不是一个答案,但它带来了另一个混淆点:

我想看看问题是编译时评估还是反射实际上允许 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() 会发生变化?

于 2009-10-23T22:11:28.713 回答
0

有一个解决方法。如果您在 static {} 块中设置私有静态 final 的值,它将起作用,因为它不会内联 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");
于 2013-12-31T13:34:50.600 回答