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 onlygetField/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 …UsergetType() 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, autounboxesEsistono 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
opensil suo package,setAccessible(true)generaInaccessibleObjectException. Le librerie richiedono di aggiungere--add-openso che il modulo apra il package. - È per oggetto. Chiamare
setAccessible(true)influisce solo sull'istanza diFieldsu cui è stato chiamato, non sul campo globalmente. UnFieldappena 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 finalprimitive oString— 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
finalnon 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.
Cosa ricavare dall'esecuzione:
toMapha prodotto uno snapshot di ogni campo d'istanza senza un singolo getter —getDeclaredFields()piùsetAccessible(true)hanno raggiunto lo statoprivatedirettamente. 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 verificatoModifier.isStatic. I serializzatori di norma saltano i campistatic,transiente sintetici; il bitset dei modificatori è il modo per prendere queste decisioni uniformemente invece di codificare i nomi dei campi. fromMapha scritto il campoprivate final currencydoposetAccessible(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 costantistatic finalnon si sarebbero mosse.- La lettura dei metadati (
bal.getType(),Modifier.toString(...),isFinal(...)) non richiedeva alcuna istanza diAccount— unFielddescrive 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 campostaticha usatocnt.get(null)— passarenullcome 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.