Modificar campos finales en Java



reflection (4)

El método set(..) Reflection funciona con FieldAccessor s.

Para int obtiene un UnsafeQualifiedIntegerFieldAccessorImpl , cuya superclase define la propiedad readOnly como verdadera solo si el campo es static y final

Entonces, para responder primero a la pregunta no formulada, aquí está la razón por la cual la final se cambia sin excepción.

Todas las subclases de UnsafeQualifiedFieldAccessor usan la clase sun.misc.Unsafe para obtener los valores. Los métodos allí son todos native , pero sus nombres son getVolatileInt(..) y getInt(..) ( getVolatileObject(..) y getObject(..) respectivamente). Los usuarios de acceso mencionados anteriormente usan la versión "volátil". Esto es lo que sucede si agregamos la versión no volátil:

System.out.println("reflection: non-volatile primitiveInt = "
     unsafe.getInt(test, (long) unsafe.fieldOffset(getField("primitiveInt"))));

(donde unsafe es instanciado por reflexión - no está permitido de otra manera) (y yo llamo getObject para Integer y String )

Eso da algunos resultados interesantes:

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

En este punto recuerdo un artículo en javaspecialists.eu que analiza un asunto relacionado. Cita JSR-133 :

Si se inicializa un campo final a una constante de tiempo de compilación en la declaración de campo, es posible que no se observen los cambios en el campo final, ya que los usos de ese campo final se reemplazan en tiempo de compilación con la constante de tiempo de compilación.

El Capítulo 9 discute los detalles observados en esta pregunta.

Y resulta que este comportamiento no es tan inesperado, ya que se supone que las modificaciones de los campos final ocurren justo después de la inicialización del objeto.

Comencemos con un caso de prueba simple:

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

A nadie le importa adivinar qué se imprimirá como salida (que se muestra en la parte inferior para no estropear la sorpresa de inmediato).

Las preguntas son:

  1. ¿Por qué los enteros primitivos y envueltos se comportan de manera diferente?
  2. ¿Por qué el acceso reflexivo frente al directo arroja resultados diferentes?
  3. El que más me molesta: ¿por qué String se comporta como primitive int y no como Integer ?

Resultados (java 1.5):

reflection: primitiveInt = 84
direct: primitiveInt = 42
reflection: wrappedInt = 84
direct: wrappedInt = 84
reflection: stringValue = 84
direct: stringValue = 42

En mi opinión, esto es aún peor: un colega señaló lo siguiente:

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

Al hacer esto, puede cambiar el comportamiento de toda la JVM en la que se está ejecutando (por supuesto, puede cambiar solo los valores para los valores entre -127 y 127).


Las constantes de tiempo de compilación están en línea (en tiempo de compilación javac). Consulte el JLS, en particular, 15.28 define una expresión constante y 13.4.9 trata sobre la compatibilidad binaria o los campos y constantes finales.

Si convierte el campo en no final o asigna una constante de tiempo de no compilación, el valor no estará en línea. Por ejemplo:

cadena final privada stringValue = null! = null? "": "42";


Esta no es una respuesta, pero trae a colación otro punto de confusión:

Quería ver si el problema era la evaluación en tiempo de compilación o si la reflexión realmente permitía a Java esquivar la palabra clave final . Aquí hay un programa de prueba. Todo lo que agregué fue otro conjunto de llamadas captadoras, por lo que hay una antes y después de cada llamada a 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());
  }
}

Aquí está la salida que obtengo (bajo 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

¿Por qué diablos cambia la llamada directa a getWrappedInt ()?





final