La Reflection in Java

Parte I: una buona conoscenza dei tipi parametricidi

L‘API reflection è un‘infrastruttura che permette ispezionare un oggetto a runtime, al fine di scoprire la classe di appartenenza, la sua composizione in termini di metodi, campi, interfacce implementate, i modificatori utilizzati e persino di lavorare su ciascuno di questi elementi in modo simile a quanto si può fare usando gli appositi operatori del linguaggio durante la stesura di un programma.

L‘API reflection

Quando si usa la reflection, non è necessario conoscere in anticipo il nome, la classe o la struttura di un oggetto: queste informazioni possono essere scoperte durante l‘esecuzione attraverso appositi metodi. La reflection è una funzionalità  fondamentale per chi desidera scrivere tools come caricatori di plugin, debuggers, ispezionatori di classi, tool per la costruzione di interfacce grafiche o più in generale per la creazione di applicazioni a componenti.

In generale la reflection non va usata alla leggera: la sua utilità  è riservata a specifici campi applicativi, ed esistono molti abili programmatori che non ne hanno mai fatto uso. Chi desidera studiare questo potente strumento dovrebbe per prima cosa aver chiara la struttura del linguaggio a livello meta-descrittivo. Si osservi la figura 1:

Una classe, che in bitecode Java può rappresentare anche un‘interfaccia, un array, una enum o un tipo di annotazione, può contenere al suo interno una collezione di campi, di costrutto-ri, di metodi o di altri oggetti class (ad esempio classi o interfacce interne). Ogni campo è caratterizzato da un nome simbolico, un tipo (il cui valore viene espresso in termini di oggetti class) e un valore, che può essere un oggetto qualunque. I metodi sono caratterizzati da un nome, un valore di ritorno, una collezione di tipi di parametro ed una di eccezioni. Inoltre la classe Method dispone di un metodo invoke(), che permette di chiamare quel particolare me-todo su un oggetto appartenente alla classe base. I costruttori sono simili ai metodi, ma non hanno valore di ritorno; al posto del metodo invoke() dispongono di un metodo newInstance(), che permette di creare nuovi oggetti di quella particolare classe.

La visione offerta fino ad ora è molto vicina alla realtà , ma per forza di cose incompleta. I dettagli verranno affrontati dapprima attraverso lo studio delle classi che compongono l‘API reflection, e quindi attraverso una serie di esempi.

Nota: con l‘arrivo di Java 5 l‘API reflection ha subito una serie di aggiunte cosଠpervasive da non permettere uno studio differenziato di quanto era possibile fare un tempo e quanto è invece possibile fare oggi. L‘aggiunta dei tipi parametrici non ha solo comportato l‘aggiunta di nuovi metodi di ispezione: alcune classi dell‘API sono state riformulate come classi parametriche. Pertanto l‘API a cui si farà  riferimento nel presente trattato è quella offerta a partire dalla versione 1.5 del JSDK.

La classe Class

La classe Class è una classe parametrica, il cui parametro T denota la classe rappresentata dall‘oggetto stesso. Un oggetto Class può rappresentare classi, interfacce, array, enum e tipi di annotazione. Persino void e i tipi primitivi sono rappresentati da oggetti Class.

Il primo metodo permette di ottenere una classe a partire dal nome completo di percorso di package (ad esempio java.awt.Button):

static Class forName(String className) 

Un gruppo di metodi premette di ottenere il nome della classe in vari formati:

String getSimpleName() String getCanonicalName() String getName() 

Il primo restituisce il nome semplice della classe, quello dichiarato nel sorgente, mentre il secondo riporta il nome completo di percorso di package e delle eventuali classi contenitore (ad esempio it.mokabyte.MyClass$MyInnerClass). Il terzo si comporta in modo differente a seconda del tipo di elemento rappresentato dall‘oggetto Class in questione. Se l‘oggetto rappresenta un elemento diverso da un array, allora viene restituito il nome binario della classe (ad esempio java.util.Vector). Se l‘oggetto rappresenta void o un tipo primitivo, viene restituita la keyword Java corrispondente. Infine se l‘oggetto rappresenta un array, viene restituita una stringa corrispondente al nome del tipo di elementi contenuti nell‘array, preceduti da un numero di caratteri ‘[‘ pari alle dimensioni dell‘array stesso (ad esempio ‘[Ljava.lang.String‘). Nella Figura 2 sono riportati tutti i possibili elementi e la relativa codifica simbolica:

Una coppia di metodi permette di ottenere un link alla superclasse o un vettore di oggetti Class corrispondenti alle interfacce implementate:

Class getSuperclass() Class[] getInterfaces() 

Un‘altra coppia di metodi permette di gestire il caso di superclassi o interfacce parametriche:

Type[] getGenericInterfaces()Type getGenericSuperclass()  

Una serie di metodi permette di conoscere se il corrente oggetto Class è un valore primitivo, un‘interfaccia, un‘array, una classe locale, una classe anonima, una classe interna, una enum o un‘annotazione:

boolean isPrimitive() boolean isInterface() boolean isArray() boolean isLocalClass() boolean isAnonymousClass() boolean isMemberClass() boolean isEnum() boolean isAnnotation() 

Se la classe rappresenta un array, il seguente metodo restituisce il tipo degli oggetti contenuti nell‘array stesso:

Class getComponentType() 

Un gruppo di metodi permette di accedere ai vari elementi public della classe, compresi quelli presenti nelle superclassi, sotto forma di array. Quando un elemento non è presente, viene restituito un vettore vuoto:

Package getPackage() Field[] getFields() Constructor[] getConstructors() Method[] getMethods() Class[] getClasses() T[] getEnumConstants() 

I seguenti metodi assomigliano ai precedenti, ma con due differenze: anzitutto restituiscono tutti gli elementi dichiarati, indipendentemente dal modificatore di protezione utilizzato; in secondo luogo essi restituiscono solamente gli elementi dichiarati nel presente oggetto Class, ma non quelli presenti nelle superclassi. Anche in questo caso, se un elemento non è presente, viene restituito un vettore vuoto:

Field[] getDeclaredFields() Constructor[] getDeclaredConstructors() Method[] getDeclaredMethods() Class[] getDeclaredClasses() 

Il prossimo metodo permette di conoscere gli eventuali tipi parametrici dichiarati nella presente classe:

TypeVariable>[] getTypeParameters() 

La classe TypeVariable verrà  approfondita più avanti. I seguenti metodi permettono di accedere ad un particolare campo, costruttore o metodo fornendo il nome e/o l‘elenco del tipo dei parametri, sotto forma di oggetti Class. Anche in questo caso vale la differenza tra i metodi getXxx() e getDeclaredXxx():

Field getField(String name) Constructor getConstructor(Class... parameterTypes) Method getMethod(String name, Class... parameterTypes) Field getDeclaredField(String name) Constructor getDeclaredConstructor(Class... parameterTypes) Method getDeclaredMethod(String name, Class... parameterTypes) 

Il seguente metodo restituisce un intero che rappresenta i modificatori della presente classe o interfaccia:

int getModifiers() 

La classe java.lang.reflect.Modifier contiene le costanti numeriche corrispondenti ai vari modificatori, nonchà© una serie di metodi di interrogazione per sapere a quale modificatore corrisponde l‘intero restituito dal metodo precedente. Si noti che ogni oggetto Class può disporre di più di un modificatore: infatti l‘intero è l‘OR logico dei valori corrispondenti ai singoli modificatori, ognuno dei quali ha un valore corrispondente ad un bit diverso.

I seguenti metodi riguardano nello specifico le classi interne o anonime, e permettono di ottenere la classe, il metodo o il costruttore contenitore:

Class getDeclaringClass() Class getEnclosingClass() Constructor getEnclosingConstructor() Method getEnclosingMethod() 

I metodi seguenti rappresentano la versione dinamica degli operatori new, instanceof e dell‘operatore di cast; il metodo asSubclass() in particolare effettua un‘operazione di casting alla superclasse specificata:

T newInstance() boolean isInstance(Object obj) T cast(Object obj)  Class  asSubclass(Class clazz) 

Il metodo seguente permette di sapere se il presente oggetto Class è superclasse (o supertinterfaccia) dell‘oggetto passato come parametro:

boolean isAssignableFrom(Class cls) 

I seguenti metodi permettono l‘ispezione delle annotazioni:

boolean isAnnotationPresent(Class annotationClass) Annotation[] getAnnotations() Annotation[] getDeclaredAnnotations() A getAnnotation(Class annotationClass) 

Infine una coppia di metodi che non hanno direttamente a che vedere con la reflection: si tratta di metodi che permettono di ottenere una file la cui posizione viene specificata, attraverso una stringa, in relazione alla posizione nel filesystem del presente oggetto class e alla sua posizione nella gerarchia dei package. Per fare un esempio, la stringa "/image/dog.gif" denota un‘immagine gif presente nella cartella image che si trova nella package root della presente classe. Questi metodi sono utili per recuperare immagini, file di configurazione o altre risorse utili in fase di esecuzione:

URL getResource(String name) InputStream getResourceAsStream(String name) 

Si passa ora ad analizzare le classi, presenti nel package java.lang.reflect, che completano il panorama degli strumenti per la reflection.

Field

Il primo metodo che verrà  illustrato permette di conoscere, a partire da un oggetto Field, la classe in cui è stato dichiarato:

Class getDeclaringClass() 

Un gruppo di metodi permette di conoscere il nome del campo, il suo tipo, i suoi modificatori e l‘eventuale tipo generico:

String getName() Class getType() int getModifiers() Type getGenericType() 

A partire da Java 1.5, un campo può rappresentare anche la costante di una enum:

boolean isEnumConstant() 

Una serie di metodi get() e set() permettono di ispezionare il contenuto del presente oggetto Field o di modificarlo. E‘ disponibile un metodo per ogni circostanza, sia per trattare Object che per tipi primitivi:

Object get(Object obj) boolean getBoolean(Object obj) byte getByte(Object obj) char getChar(Object obj) double getDouble(Object obj) float getFloat(Object obj) int getInt(Object obj) long getLong(Object obj) short getShort(Object obj) void set(Object obj, Object value) void setBoolean(Object obj, boolean z) void setByte(Object obj, byte b) void setChar(Object obj, char c) void setDouble(Object obj, double d) void setFloat(Object obj, float f) void setInt(Object obj, int i) void setLong(Object obj, long l) void setShort(Object obj, short s) 

Seguono i consueti metodi che permettono di ispezionare le eventuali annotazioni:

boolean isAnnotationPresent(Class annotationClass) Annotation[] getAnnotations() Annotation[] getDeclaredAnnotations() A getAnnotation(Class annotationClass) 

Infine una coppia di metodi permette di ottenere una rappresentazione sotto forma di stringa, con o senza la dichiarazione di tipi parametrici:

String toGenericString() String toString() 

Classe Constructor

La classe Constructor è una classe parametrica al cui tipo T corrisponde a quello della classe di appartenenza. Anche questa classe dispone di un metodo che permette di ottenere la classe a cui il costruttore appartiene:

Class getDeclaringClass() 

Segue la descrizione dei metodi che permettono l‘ispezione del presente oggetto, in modo da conoscerne il nome, i modificatori, il tipo dei parametri (semplici o generici), le eventuali eccezioni generate (semplici o parametriche) e i tipi parametrici dichiarati dal presente oggetto Constructor:

String getName() int getModifiers() Class[] getParameterTypes() Type[] getGenericParameterTypes() Class[] getExceptionTypes() Type[] getGenericExceptionTypes() TypeVariable>[] getTypeParameters() 

In tutti i casi in cui i precedenti metodi restituiscono un array, questo avrà  lunghezza 0 se l‘elemento richiesto non è presente. Un apposito metodo permette di sapere se il presente costruttore accetta o meno un numero variabile di argomenti:

boolean isVarArgs() 

Una serie di metodi permette di gestire le eventuali annotazioni. Rispetto ai metodi già  visti su Field, si noti il metodo getParameterAnnotations(), che restituisce un array di array che contiene le eventuali annotazioni dei parametri formali, in ordine di dichiarazione:

boolean isAnnotationPresent(Class annotationClass) Annotation[] getAnnotations() Annotation[] getDeclaredAnnotations() A getAnnotation(Class annotationClass) Annotation[][] getParameterAnnotations() 

Il metodo newInstance() permette di creare un oggetto di tipo T a partire dal presente oggetto Constructor con i parametri adeguati:

T newInstance(Object... initargs) 

L‘ultimo metodo segnalato permette di avere una rappresentazione in formato stringa del costruttore rappresentato dal presente oggetto, comprensivo di eventuali tipi parametrici:

String toGenericString() 

Classe Method

I metodi della classe Method sono in gran parte comuni a quelli della classe Constructor, con l‘unica differenza che un metodo prevede anche un tipo di ritorno, ragion per cui la classe Method contiene un apposito metodo getReturnType():

Class getDeclaringClass() Class getReturnType() Type getGenericReturnType() String getName() int getModifiers() Class[] getParameterTypes() Type[] getGenericParameterTypes() Class[] getExceptionTypes() Type[] getGenericExceptionTypes() TypeVariable[] getTypeParameters() boolean isVarArgs() 

Nel caso il presente oggetto Method rappresenti il metodo di un tipo di annotazione, è disponibile un apposito metodo per ispezionare l‘eventuale valore di default:

Object getDefaultValue() 

Seguono i metodi per ispezionare le eventuali annotazioni, corrispondenti in tutto e per tutto a quelli della classe Constructor:

boolean isAnnotationPresent(Class annotationClass) Annotation[] getAnnotations() Annotation[] getDeclaredAnnotations() A getAnnotation(Class annotationClass)Annotation[][] getParameterAnnotations() 

Il metodo toGenericString() permette di ottenere una rappresentazione in formato stringa del metodo sottostante:

String toGenericString() 

Infine un metodo invoke() permette di invocare il presente metodo su un oggetto specificato da un apposito parametro con gli argomenti opportuni:

Object invoke(Object obj, Object... args) 

Classe Array

La classe Array comprende solamente metodi statici, che permettono di operare su oggetti Class di tipo array. Il primo metodo che verrà  illustrato permette di conoscere la lunghezza di un array:

static int getLength(Object array) 

Seguono una coppia di metodi che permettono di creare un array a partire da un parametro di tipo Class che ne denota il tipo e da un intero lenght che ne specifica la lunghezza. La seconda versione del metodo newInstance() permette di creare array di array, specificando l‘insieme delle lunghezze attraverso un array di interi:

static Object newInstance(Class componentType, int length) static Object newInstance(Class componentType, int[] dimensions) 

Infine una serie di metodi get() e set() permettono di leggere o di impostare il valore di un elemento di un array, specificandone il valore e l‘indice:

static Object get(Object array, int index) static boolean getBoolean(Object array, int index) static byte getByte(Object array, int index) static char getChar(Object array, int index) static double getDouble(Object array, int index) static float getFloat(Object array, int index) static int getInt(Object array, int index) static long getLong(Object array, int index) static short getShort(Object array, int index) static void set(Object array, int index, Object value) static void setBoolean(Object array, int index, boolean z) static void setByte(Object array, int index, byte b) static void setChar(Object array, int index, char c) static void setDouble(Object array, int index, double d) static void setFloat(Object array, int index, float f) static void setInt(Object array, int index, int i) static void setLong(Object array, int index, long l) static void setShort(Object array, int index, short s) 

Interfaccia Annotation

L‘interfaccia Annotation viene implementata dalle classi che rappresentano uno specifico tipo di annotazione. L‘unico metodo definito da questa interfaccia è il seguente:

Class annotationType() 

Questo metodo restituisce l‘oggetto Class che rappresenta l‘annotazione vera e propria, che può avere i propri modificatori, le proprie annotazioni e i propri metodi, che a loro volta possono avere un valore di default.

Classe Package

Il package è denotato da un nome, che segue la dot notation caratteristica di Java (ad esempio java.io o javax.swing);

String getName() 

I seguenti metodi permettono di accedere ad una serie di informazioni di carattere commerciale legate al venditore del package, al titolo dell‘implementazione, e alla versione:

String getImplementationVendor() String getImplementationTitle() String getImplementationVersion() 

Qualora il package si rifaccia ad una specifica, è possibile recuperare le informazioni corrispondenti al venditore della specifica, al titolo e alla versione:

String getSpecificationVendor() String getSpecificationTitle() String getSpecificationVersion() 

Qualora il presente package sia l‘implementazione di una particolare specifica, si può verificare l‘eventuale compatibilità  con una versione di riferimento specificata dal parametro:

boolean isCompatibleWith(String desired) 

Seguono i consueti metodi per l‘ispezione delle eventuali annotazioni:

boolean isAnnotationPresent(Class annotationClass)   A getAnnotation(Class annotationClass) Annotation[] getAnnotations() Annotation[] getDeclaredAnnotations() 

Quindi il caratteristico metodo toString():

String toString() 

Infine una coppia di metodi statici che permettono di recuperare un package a partire da un nome o l‘elenco di tutti i packages accessibili al ClassLoader:

static Package getPackage(String name) static Package[] getPackages() 

Type

L‘introduzione dei tipi parametrici in Java 5 ha richiesto la definizione di un‘apposita interfaccia Type, peraltro vuota, e di quattro sue sottoclassi con delle forti interazioni reciproche, come si vede in figura 3:

Il diagramma delle classi di Type è molto complesso, e riflette la complessità  della metarappresentazione dei tipi parametrici.

GenericArrayType

Il linguaggio Java 5 permette di definire array di tipo parametrico. Un apposito metodo permette di conoscere il tipo generico dell‘array stesso:

Type getGenericComponentType() 

ParametryzedType

L‘interfaccia ParametryzedType contiene tutti i metodi necessari ad ispezionare un tipo parametrico. Il primo di questi permette di conoscere il tipo reale dei tipi parametrici dichiarati:

Type[] getActualTypeArguments() 

A questo proposito è bene ricordare che il tipo parametrico reale è noto solamente a runtime, un fattore che giustifica l‘importanza del metodo precedente. Il metodo seguente restituisce il tipo che racchiude il presente tipo, permettendo in questo modo di ispezionare i tipi parametrici nidificati:

Type getOwnerType() 

L‘ultimo dei metodi di questa interfaccia permette di conoscere l‘oggetto Type che rappresenta la classe o l‘interfaccia che ha dichiarato il presente tipo:

Type getRawType() 

A questo proposito si ricorda che la classe Class implementa a sua volta l‘interfaccia Type.

TypeVariable

L‘interfaccia TypeVariable è un‘interfaccia parametrica il cui tipo parametrico formale D rappresenta un oggetto di tipo GenericDeclaration, che verrà  illustrato in seguito. Il primo metodo di questa interfaccia restituisce un array di oggetti Type che denota i limiti superiori dell‘oggetto Type corrente:

Type[] getBounds() 

Il prossimo metodo restituisce il nome di questo tipo parametrico, cosଠcome appare nel codice sorgente:

String getName() 

Infine un metodo che restituisce un oggetto GenericDeclaration che rappresenta la dichiarazione generica del presente oggetto TypeVariable:

D getGenericDeclaration() 

L‘interfaccia GenericDeclaration contiene un solo metodo:

TypeVariable[] getTypeParameters()

Questo metodo permette di ottenere un array di TypeVariable che rappresentano le dichiarazioni di tipi parametrici dichiarati dal presente oggetto GenericDeclaration in ordine di dichiarazione.

WildcardType

L‘interfaccia WildcardType denota il carattere jolly ‘?‘ e le sue applicazioni all‘interno delle dichiarazioni di tipi parametrici (ad esempio , o ) per specificare limiti superiori o limiti inferiori:

Type[] getLowerBounds() Type[] getUpperBounds() 

Conclusioni

Siamo finalmente giunti al termine di questa lunga carrellata sulle classi che forniscono l‘infrastruttura per la reflection. Il prossimo mese sarà  possibile vedere un utilizzo pratico di tali classi per ispezionare oggetti a runtime.

Condividi

Pubblicato nel numero
101 novembre 2005
Andrea Gini è un professionista del settore aerospaziale, un consulente IT e un giornalista scientifico. La collaborazione con MokaByte, iniziata nel lontano 1999, è stata l‘unica costante in un percorso professionale che lo ha portato dalla fotografia professionale alla scienza spaziale. Attualmente lavora al Payload Safety Review Panel presso il…
Articoli nella stessa serie
Ti potrebbe interessare anche