Introduzione alla Reflection in Java
Cos'è la reflection in Java, quando usarla e panoramica del pacchetto java.lang.reflect.
La Reflection è la capacità di un programma di ispezionare e manipolare la propria struttura a runtime: chiedere a una classe quali campi, metodi e costruttori possiede, leggere e scrivere quei campi, invocare quei metodi e creare nuove istanze — il tutto senza nominare i tipi in fase di compilazione. Dove il normale Java è statico (il compilatore conosce ogni tipo che tocchi), la reflection è dinamica (i tipi vengono scoperti da stringhe, configurazioni o da ciò che viene caricato a runtime). Questa parte del libro è una panoramica di java.lang.reflect; questo capitolo introduce il contesto.
Cosa permette di fare la reflection
Il codice normale nomina il tipo con cui lavora:
User u = new User("ada");
String name = u.getName();Il codice riflessivo raggiunge lo stesso risultato senza scrivere User o getName come token a tempo di compilazione — sono stringhe risolte a runtime:
Class<?> cls = Class.forName("com.example.User");
Object u = cls.getDeclaredConstructor(String.class).newInstance("ada");
Object name = cls.getMethod("getName").invoke(u);La seconda forma è molto più verbosa e molto più lenta, e scarta il controllo dei tipi a tempo di compilazione. Non la scriveresti mai per la logica applicativa ordinaria. Vi si ricorre precisamente quando il tipo non è noto fino a runtime.
Quando la reflection vale la pena
La reflection è il motore dei framework, non del codice quotidiano. Utilizzi tipici:
- Dependency injection (Spring, Guice, CDI): il container legge le annotazioni e le firme dei costruttori, poi istanzia e collega bean di cui non ha mai visto il sorgente.
- Serializzazione (Jackson, Gson): una libreria JSON scorre i campi di un oggetto per leggerli o popolarli senza che tu scriva codice di mapping per ogni classe.
- ORM (Hibernate, JPA): mappa le colonne ai campi riflettendo su una classe entità.
- Test runner (JUnit): trova i metodi annotati con
@Teste li invoca. - Sistemi di plugin: carica una classe indicata in un file di configurazione e chiama su di essa un metodo di un'interfaccia nota.
Il filo conduttore: un meccanismo generale che opera su tipi utente arbitrari contro cui non è stato compilato. È esattamente il problema che la reflection risolve.
I tipi principali in java.lang.reflect
La reflection parte da un oggetto Class (trattato nel capitolo successivo, Java Class Objects) e si ramifica in una piccola famiglia di classi, ognuna delle quali descrive un tipo di membro:
| Tipo | Rappresenta | Ottenuto da Class tramite |
|---|---|---|
Field | un campo | getField / getDeclaredField(s) |
Method | un metodo | getMethod / getDeclaredMethod(s) |
Constructor<T> | un costruttore | getConstructor / getDeclaredConstructor(s) |
Parameter | un parametro di metodo/costruttore | Executable.getParameters() |
Modifier | un helper per il bitset int dei modificatori | metodi statici |
Field, Method e Constructor condividono una gerarchia di superclassi comune: Member (l'interfaccia) e AccessibleObject (che porta setAccessible). Questa base condivisa è il motivo per cui "renderlo accessibile" e "leggerne le annotazioni" appaiono identici indipendentemente dal membro che si ha in mano.
La distinzione tra get… e getDeclared…
Quasi ogni metodo di ricerca esiste in due varianti, e la distinzione è importante in ogni capitolo successivo:
getField/getMethod/getConstructor— restituisce i membri pubblici, inclusi quelli ereditati da superclassi e interfacce.getDeclaredField/getDeclaredMethod/getDeclaredConstructor— restituisce i membri di qualsiasi livello di accesso (private,protected, package), ma solo quelli dichiarati su questa classe esatta — niente di ereditato.
Quindi getMethods() vede un metodo pubblico ereditato da un genitore ma non un helper private sulla classe stessa; getDeclaredMethods() vede l'helper private ma non il metodo pubblico ereditato. Per raggiungere un membro privato ereditato si risale con getSuperclass() chiamando getDeclared… a ogni livello.
Il costo: velocità, sicurezza ed encapsulation
La reflection è potente, e ogni potere ha un prezzo.
- Prestazioni. Le chiamate riflessive sono più lente delle chiamate dirette — ricerche di metodi/campi, controlli di accesso e boxing degli argomenti aggiungono tutti overhead. Il JIT ottimizza bene le chiamate riflessive frequenti, ma la reflection in un ciclo stretto è un segnale d'allarme. Metti in cache gli oggetti
Method/Field; non ricercarli a ogni chiamata. - Nessuna sicurezza a tempo di compilazione. Un errore di battitura nel nome di un metodo viene compilato senza problemi e fallisce a runtime con
NoSuchMethodException. Gli strumenti di refactoring rinominanogetNameovunque — tranne all'interno della stringa"getName". - Violazione dell'encapsulation.
setAccessible(true)permette di leggere e scrivere lo statoprivate. È così che i serializzatori popolano i campi senza setter, ma questo ti accoppia a dettagli interni che l'autore della classe non ha mai promesso di mantenere stabili. - Restrizioni del sistema di moduli. Da Java 9, il sistema di moduli può negare l'accesso riflessivo ai pacchetti non esportati. Chiamare
setAccessible(true)attraverso un confine di modulo che non haopensil pacchetto lanciaInaccessibleObjectException.
Un esempio pratico: un piccolo dump generico di oggetti
Per rendere concreto l'ambito, ecco una routine riflessiva che stampa i campi e i valori di qualsiasi oggetto — il tipo di cosa che fa un debugger o una libreria di logging. Non nomina alcun tipo applicativo; funziona su qualunque cosa le venga passata.
Cosa ricavare dall'esecuzione:
- Il metodo
dumpnon ha nominato alcun tipo concreto eppure ha stampato siaPointcheUser. Il suo unico contratto è "passami unObject"; tutto sulla struttura — nomi dei campi, tipi, valori — proveniva dagetClass()a runtime. Questa è la mossa fondamentale della reflection: una routine, input arbitrari. getDeclaredFields()ha restituito tutti i campi inclusi quelliprivate, ma per leggerli era necessario chiamare primasetAccessible(true). Senza quella chiamata,f.get(obj)su un campo privato lanciaIllegalAccessException. La ricerca e l'accesso sono due controlli separati.Modifier.toString(f.getModifiers())ha trasformato il bitset grezzo dei modificatori in testo leggibile comeprivate final. I modificatori sono memorizzati come unintdi bit di flag; l'helperModifierli decodifica così non devi testare i bit manualmente.- Il terzo oggetto è stato costruito senza alcun
new User(...)nel sorgente —Class.forName("…$User")ha risolto la classe annidata da una stringa (nota il separatore$per i tipi annidati), egetDeclaredConstructor(...).newInstance(...)lo ha costruito. Questo è il pattern di caricamento dei plugin in miniatura: un nome in entrata, un oggetto in uscita. - Leggere il valore del campo (
f.get(obj)) e leggere i metadati del campo (f.getName(),f.getModifiers()) sono operazioni indipendenti. I metadati non richiedono né istanza né accessibilità; i valori richiedono l'oggetto e, per i campi privati, il flag di accessibilità.
Come è organizzato il resto di questa parte
Ogni capitolo rimanente approfondisce un aspetto specifico:
- Class objects — i tre modi per ottenere un
Class<T>e cosa rivela. - Fields — ispezione, lettura e scrittura dei campi (inclusi
privateefinal). - Methods — trovare e invocare metodi, risoluzione degli overload, valori di ritorno.
- Constructors — costruire istanze in modo riflessivo, inclusi i costruttori privati.
- Annotations — leggere i metadati allegati con le annotazioni, a runtime.
- Dynamic proxies — sintetizzare intere implementazioni di interfacce a runtime.
Per tutto il corso, tieni presente il compromesso: la reflection è lo strumento giusto quando il tipo non è genuinamente noto fino a runtime, e quello sbagliato quando lo è. Il capitolo successivo parte dalla radice di tutto — l'oggetto Class.