W3docs

Java Reflection: Ispezione dei campi

Ispeziona, leggi e modifica i campi a runtime in Java con l'API di reflection.

Un oggetto Field descrive un campo di una classe: il suo nome, il suo tipo, i suoi modificatori e — dato un'istanza — il suo valore. La reflection consente di elencare i campi di una classe, leggerli e scriverli, anche quando sono private e non hanno getter o setter. È esattamente così che i deserializzatori JSON popolano gli oggetti e gli ORM idratano le entità. Questo capitolo tratta come ottenere oggetti Field, leggere e scrivere valori, il meccanismo setAccessible e il caso speciale dei campi final.

Questo capitolo si basa sull'introduzione alla reflection. Per le API correlate, consulta l'ispezione dei metodi e l'ispezione dei costruttori.

Ottenere oggetti Field

La stessa distinzione pubblico/dichiarato dell'introduzione si applica anche qui:

Class<?> c = User.class;

Field f1 = c.getField("name");             // public field, incl. inherited — else NoSuchFieldException
Field f2 = c.getDeclaredField("name");     // any access level, this class only

Field[] all   = c.getFields();             // public fields, incl. inherited
Field[] mine  = c.getDeclaredFields();     // all access levels, this class only

getField/getFields vedono solo i campi public ma seguono la catena di ereditarietà. getDeclaredField/getDeclaredFields vedono anche i campi private/protected/package, ma solo quelli dichiarati letteralmente nella classe richiesta. Per raccogliere ogni campo inclusi quelli privati ereditati, è necessario scorrere getSuperclass() e unirli.

Metadati di un Field: nome, tipo, modificatori, generics

Un Field risponde a domande su se stesso senza aver bisogno di alcuna istanza:

Field f = User.class.getDeclaredField("age");

f.getName();           // "age"
f.getType();           // int.class           — the erased type
f.getGenericType();    // int                 — Type, keeps generic info
f.getModifiers();      // int bitset
Modifier.isPrivate(f.getModifiers());   // true/false
Modifier.isStatic(f.getModifiers());
Modifier.isFinal(f.getModifiers());
f.getDeclaringClass(); // class …User

getType() restituisce la Class cancellata (List); getGenericType() restituisce un Type che, per un campo List<String>, puoi eseguire il downcast a ParameterizedType per recuperare String. Questo recupero funziona perché le firme generiche dei campi sono mantenute nel file di classe anche se le istanze sono cancellate.

Leggere e scrivere valori

Per leggere o scrivere, hai bisogno di un'istanza (o null per un campo static) e devi superare il controllo di accesso:

User u = new User("ada", 36);
Field age = User.class.getDeclaredField("age");
age.setAccessible(true);               // bypass the access check for private

int current = age.getInt(u);           // typed getter for primitives → 36
age.setInt(u, 37);                     // typed setter
Object boxed = age.get(u);             // generic getter, autoboxes → Integer 37
age.set(u, 40);                        // generic setter, autounboxes

Esistono accessor tipizzati — getInt, getBoolean, getDouble, setLong, … — per i campi primitivi, e il generico get(Object)/set(Object,Object) per qualsiasi campo (con boxing dei primitivi). Per un campo static, passa null come target: staticField.get(null).

Il meccanismo setAccessible

Per impostazione predefinita, un Field applica le regole di accesso di Java: leggere in modo riflessivo un campo private genera IllegalAccessException. field.setAccessible(true) sopprime quel controllo per questo oggetto Field. È ciò che permette alla reflection di accedere agli internals — e ciò che la rende pericolosa.

Due avvertenze a partire da Java 9:

  • Confini dei moduli. Se il tipo di destinazione si trova in un modulo che non ha opens il suo package, setAccessible(true) genera InaccessibleObjectException. Le librerie richiedono di aggiungere --add-opens o che il modulo apra il package.
  • È per oggetto. Chiamare setAccessible(true) influisce solo sull'istanza di Field su cui è stato chiamato, non sul campo globalmente. Un Field appena ottenuto per lo stesso membro parte nuovamente bloccato.

Scrivere campi final

I campi final sono un caso speciale e delicato. Per un campo final non statico a volte è ancora possibile scriverlo dopo setAccessible(true):

Field f = Config.class.getDeclaredField("name");   // private final String
f.setAccessible(true);
f.set(config, "changed");                            // may work…

Ma ci sono importanti avvertenze:

  • Non funziona per le costanti static final primitive o String — queste vengono inlinate dal compilatore in ogni punto di utilizzo, quindi anche se modifichi il campo, le letture già compilate non lo rifletteranno.
  • La JVM e il JIT assumono che i campi final non cambino mai; mutarne uno è un comportamento indefinito per la visibilità e può essere ottimizzato via.
  • Le JDK moderne lo vietano sempre più esplicitamente.

La regola onesta è: non mutare i campi final tramite reflection in produzione. I framework di serializzazione che lo fanno (per ricostruire oggetti immutabili) usano la macchina di basso livello Unsafe/VarHandle e accettano il rischio deliberatamente. L'esempio seguente mostra il caso instance-final funzionante per illustrare il meccanismo, non come raccomandazione.

Un esempio pratico: un piccolo mapper basato sui campi

Il programma riflette sui campi dichiarati di un oggetto per costruire una Map<String,Object> (un mini serializzatore), poi prende una mappa e scrive i suoi valori in una nuova istanza (un mini deserializzatore) — accedendo ai campi private senza getter o setter.

java— editable, runs on the server

Cosa ricavare dall'esecuzione:

  • toMap ha prodotto uno snapshot di ogni campo d'istanza senza un singolo getter — getDeclaredFields() più setAccessible(true) hanno raggiunto lo stato private direttamente. Questo è, meccanicamente, ciò che fanno Jackson e Gson quando configurati per l'accesso ai campi. La classe non ha bisogno di API speciali; la reflection ne fornisce una generica.
  • Il campo statico count è stato escluso perché il ciclo ha verificato Modifier.isStatic. I serializzatori di norma saltano i campi static, transient e sintetici; il bitset dei modificatori è il modo per prendere queste decisioni uniformemente invece di codificare i nomi dei campi.
  • fromMap ha scritto il campo private final currency dopo setAccessible(true) e l'effetto è stato applicato — dimostrando il meccanismo instance-final. Questo ha funzionato solo perché currency è un final non statico riassegnato prima che qualsiasi ottimizzatore lo assumesse costante; fare affidamento su questo in codice reale è fragile, e le costanti static final non si sarebbero mosse.
  • La lettura dei metadati (bal.getType(), Modifier.toString(...), isFinal(...)) non richiedeva alcuna istanza di Account — un Field descrive la dichiarazione, che è la stessa per ogni oggetto della classe. I valori richiedono un'istanza; la struttura no.
  • Il tipizzato getInt(rebuilt) ha restituito il primitivo direttamente senza boxing, e la lettura del campo static ha usato cnt.get(null) — passare null come target è la convenzione per i campi statici. Scegliere l'accessor tipizzato per i primitivi evita un'allocazione per ogni lettura, il che è importante nei percorsi di serializzazione hot.

Esercizio

Pratica
Una libreria JSON deserializza in una classe con campi 'private' e nessun setter. Usando la reflection chiama 'getDeclaredField(name)' poi 'field.set(obj, value)', ma ottiene 'IllegalAccessException' sul primo campo privato. Quale singola chiamata aggiuntiva risolve il problema e perché è necessaria?
Una libreria JSON deserializza in una classe con campi 'private' e nessun setter. Usando la reflection chiama 'getDeclaredField(name)' poi 'field.set(obj, value)', ma ottiene 'IllegalAccessException' sul primo campo privato. Quale singola chiamata aggiuntiva risolve il problema e perché è necessaria?
Was this page helpful?