2009-10-23 22 views
50

Cominciamo con un semplice caso di test:Modifica dei campi finali in 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()); 
    } 
} 

Chiunque cura di intuire ciò che verrà stampato come uscita (mostrato in basso da non rovinare subito la sorpresa).

Le domande sono:

  1. Perché primitiva e avvolto intero si comportano in modo diverso?
  2. Perché l'accesso diretto riflessivo vs restituisce risultati diversi?
  3. Quello che mi affligge di più - perché String si comporta come il primitivo int e non come Integer?

Risultati (Java 1.5):

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

+1: Piacere di leggere l'argomento. –

+0

questo potrebbe essere strettamente correlato al compilatore. – andy

risposta

21

costanti di tempo di compilazione sono inline (in javac tempo di compilazione). Vedere il JLS, in particolare 15.28 definisce un'espressione costante e 13.4.9 discute la compatibilità binaria o i campi e le costanti finali.

Se si rende il campo non definitivo o si assegna una costante di tempo non compilata, il valore non è in linea. Ad esempio:

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

+0

Così sono. Per il primitivo int mi aspettavo il risultato che ho ottenuto. È una corda che mi lascia perplesso. Hai un legame più concreto con JLS in cui è descritto? – ChssPly76

+0

Questa è sicuramente una parte della risposta, ma come è possibile che se si accede alla stringa tramite il campo reflection, si ottiene un valore diverso rispetto a se si chiede direttamente all'oggetto? –

+0

ChssPly76: aggiunte le due sezioni interessanti. Steve B: Se si utilizza il riflesso si torna al valore che il file di classe riflesso ha impostato su quel campo. Se si fa riferimento direttamente, il valore viene copiato in (javac) in fase di compilazione (purché si tratti di una costante in fase di compilazione). –

1

Questa non è una risposta, ma porta un altro punto di confusione:

ho voluto vedere se il problema fosse di valutazione in fase di compilazione o se il riflesso è stato effettivamente permettendo Java per aggirare la parola chiave final. Ecco un programma di test. Tutto quello che ho aggiunto era un altro gruppo di chiamate getter, quindi ce n'è uno prima e dopo ogni chiamata 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()); 
    } 
} 

Ecco l'uscita ottengo (sotto 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 

Perché diavolo fa la chiamata diretta al getWrappedInt() cambiare?

+5

La vera domanda è "perché gli altri due non cambiano?" E la risposta è - perché i loro valori sono sottolineati nei getter (la risposta di Tom rimanda alle sezioni JLS che descrivono questo comportamento).L'allineamento avviene solo per i tipi primitivi e String (assumendo che le altre condizioni come l'espressione finale/costante/ecc ... siano soddisfatte). Così int e String restituiscono 42 dai getter; L'intero no. I ** valori ** effettivi sono stati modificati per tutti e 3 i campi. Se questo è fonte di confusione - ed è :-) - decompila la classe e vedrai cosa intendo. – ChssPly76

+0

ah, bizzarro ........ –

6

Il metodo set(..) di Reflection funziona con FieldAccessor s.

Per int si ottiene un UnsafeQualifiedIntegerFieldAccessorImpl, la cui superclasse definisce la proprietà readOnly per essere vero solo se il campo è siastatic e final

Quindi, per rispondere prima alla domanda inespressa - ecco perché il final viene modificato senza eccezione.

Tutte le sottoclassi di UnsafeQualifiedFieldAccessor utilizzano la classe sun.misc.Unsafe per ottenere i valori. I metodi sono tutti native, ma i loro nomi sono getVolatileInt(..) e getInt(..) (getVolatileObject(..) e getObject(..) rispettivamente). Gli accessor menzionati utilizzano la versione "volatile".Ecco cosa succede se si aggiunge la versione non-volatile:

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

(dove unsafe viene istanziato per riflessione - non è consentito in altro modo) (e che io chiamo getObject per Integer e String)

Che dà alcuni risultati interessanti:

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 

a questo punto mi ricordano an article at javaspecialists.eu discutere una questione relativa. Si cita JSR-133:

Se un campo finale viene inizializzato a una costante nella dichiarazione campo in fase di compilazione, può non essere rispettato modifiche al campo finale, dal momento che usi di quel campo finale sono sostituiti al momento della compilazione con il costante di compilazione.

Il capitolo 9 tratta i dettagli osservati in questa domanda.

E si scopre che questo comportamento non è così inaspettato, dal momento che le modifiche dei campi final si verificano solo dopo l'inizializzazione dell'oggetto.

+1

per leggere 'wrappedInt' puoi usare' getObject' invece di 'getInt' –

+0

grazie, è corretto. Ho aggiornato la risposta. – Bozho

10

A mio parere questo è ancora peggio: Un collega ha sottolineato la seguente cosa buffa:.

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

In questo modo, è possibile modificare il comportamento di tutta la JVM è in esecuzione in (ovviamente è possibile modificare solo i valori per i valori compresi tra -127 e 127)

+1

Questo merita un uptote solo per pura malvagità. –

+0

In realtà penso che gli interi potrebbero avere un valore per il limite superiore della cache, quindi potresti andare più in alto di così! –

+0

Domanda intervista :) – TWiStErRob

1

C'è una soluzione per questo. se si imposta il valore della finale statica privata archiviato nel blocco statico {} funzionerà perché non sarà in linea il campo:

private static final String MY_FIELD; 

static { 
    MY_FIELD = "SomeText" 
} 

... 

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

field.setAccessible(true); 
field.set(field, "fakeText"); 
Problemi correlati