2009-10-23 8 views
50

Vamos a empezar con un caso de prueba sencilla:Modificación de campos finales en 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()); 
    } 
} 

Cualquiera cuidado para adivinar lo que va a ser impreso como salida (que se muestra en la parte inferior, para no estropear la sorpresa de inmediato).

Las preguntas son:

  1. ¿Por qué número entero primitiva y envuelto comportan de manera diferente?
  2. ¿Por qué el acceso directo reflectante arroja resultados diferentes?
  3. El que más me molesta, ¿por qué String se comporta como primitivo 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 
+5

+1: agradable leer su tema. –

+0

esto puede estar estrechamente relacionado con el compilador. – andy

Respuesta

21

constantes de tiempo de compilación se inline (en javac tiempo de compilación). 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 realiza el campo no final o asigna una constante de tiempo no de compilación, el valor no está en línea. Por ejemplo:

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

+0

Entonces lo son. Para primitive int esperaba el resultado que obtuve. Es String lo que me desconcierta. ¿Tiene un vínculo más concreto con JLS donde se describe? – ChssPly76

+0

Esto es ciertamente parte de la respuesta, pero ¿cómo es que si ACCES la cadena a través de la reflexión field.get se obtiene un valor diferente que si usted le pide al objeto directamente? –

+0

ChssPly76: se agregaron las dos secciones interesantes. Steve B: si usa la reflexión, vuelve al valor que el archivo de clase reflejado ha establecido en ese campo. Si hace referencia a él directamente, el valor se copia al (javac) en tiempo de compilación (siempre que es una constante en tiempo de compilación). –

1

Ésta no es una respuesta, sino que nos lleva a 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 fue en realidad permitiendo Java para moverse por 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 de recibo (en 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 hace la llamada directa a getWrappedInt() cambiar?

+5

La verdadera pregunta es "¿por qué los otros dos no cambian?" Y la respuesta es: porque sus valores están en línea en captadores (la respuesta de Tom apunta a las secciones de JLS que describen este comportamiento).La alineación solo ocurre para los tipos primitivos y String (suponiendo que las otras condiciones como expresión final/constante/etc ... están satisfechas). Así int y String return 42 de getters; El entero no. Los valores ** reales ** se han cambiado para los 3 campos. Si esto es confuso, y es :-) - descompila la clase y verás a qué me refiero. – ChssPly76

+0

ah, extraño ........ –

6

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

Para int se pone un UnsafeQualifiedIntegerFieldAccessorImpl, cuya superclase define la propiedad readOnly para ser verdad sólo si el campo es tantostatic y final

Así que para responder en primer lugar la pregunta sin respuesta - he aquí por qué 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".Aquí es lo que sucede si añadimos la versión no volátil:

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

(donde unsafe se crea una instancia de reflexión - no se permite otro tipo) (y yo llamamos 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 me recuerdan an article at javaspecialists.eu discutir un asunto relacionado. Se cita a JSR-133:

Si un campo final se inicializa en una constante en la declaración de campo en tiempo de compilación, no pueden observarse cambios en el campo final, ya usos de ese campo final se reemplazan en tiempo de compilación con el Constante de tiempo de compilación.

capítulo 9 se analizan los datos 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.

+1

para leer el 'wrappedInt' puede usar' getObject' en lugar de 'getInt' –

+0

gracias, eso es correcto. Actualicé la respuesta. – Bozho

10

En mi opinión, esto es aún peor: Un colega señaló a la siguiente cosa curiosa:.

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

De esta manera, se puede cambiar el comportamiento de toda la JVM está ejecutando en (por supuesto sólo se puede modificar los valores para los valores entre -127 y 127)

+1

Esto merece un voto positivo solo por pura maldad. –

+0

En realidad, creo que los enteros pueden tener un valor para el límite superior de la memoria caché, ¡así que podrías ir más allá de eso! –

+0

Pregunta de entrevista :) – TWiStErRob

1

Hay una solución alternativa para esto. si establece el valor del archivo estático privado archivado en el bloque {} estático funcionará porque no insertará el archivo:

private static final String MY_FIELD; 

static { 
    MY_FIELD = "SomeText" 
} 

... 

Field field = VisitorId.class.getDeclaredField("MY_FIELD"); 

field.setAccessible(true); 
field.set(field, "fakeText"); 
Cuestiones relacionadas