Изменение окончательных полей в Java
Начнем с простого тестового примера:
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());
}
}
Кто-нибудь хочет догадаться, что будет напечатано как результат (показано внизу, чтобы не сразу портить сюрприз).
Вопросы:
- Почему примитивное и завернутое целое поведение ведет себя по-другому?
- Почему рефлексивный и прямой доступ возвращают разные результаты?
- Тот, который меня больше всего волнует: почему String ведет себя как примитив
int
, а не как Integer
?
Результаты (java 1.5):
reflection: primitiveInt = 84
direct: primitiveInt = 42
reflection: wrappedInt = 84
direct: wrappedInt = 84
reflection: stringValue = 84
direct: stringValue = 42
Ответы
Ответ 1
Константы времени компиляции встроены (в javac compile-time). См. JLS, в частности 15.28 определяет константное выражение, а 13.4.9 обсуждает двоичную совместимость или конечные поля и константы.
Если вы сделаете поле не последним или назначьте некомпилирующую постоянную времени, значение не будет включено. Например:
private final Строка stringValue = null!= null? ":" 42";
Ответ 2
По-моему, это еще хуже: коллега указал на следующую забавную вещь:
@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)
Ответ 3
Метод отражения 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
должны выполняться только после инициализации объекта.
Ответ 4
Это не ответ, но он вызывает еще одну путаницу:
Мне хотелось узнать, была ли проблема оценкой времени компиляции или было ли отражение фактически позволять Java обойти ключевое слово final
. Вот тестовая программа. Все, что я добавил, это еще один набор геттер-вызовов, так что там до и после каждого вызова 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()?
Ответ 5
Для этого есть работа. если вы установите значение частного статического окончательного файла в статическом блоке {}, оно будет работать, потому что оно не будет встраивать файл 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");