MokaByte 101 - 9mbre 2005
 
MokaByte 101 - 9mbre 2005 Prima pagina Cerca Home Page

 

 

 

La reflection in Java
I parte

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. 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

L'API reflection

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:


Figura 1
- Meta rappresentazione UML

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 costruttori, 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 metodo 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<T>
La classe Class<T> è 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 Tavola 1 sono riportati tutti i possibili elementi e la relativa codifica simbolica:



Tavola 1: Codifica simbolica usata dal metodo getName() di Class

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

Class<? super T> 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<Class<T>>[] 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<T> getConstructor(Class... parameterTypes)
Method getMethod(String name, Class... parameterTypes)
Field getDeclaredField(String name)
Constructor<T> 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)
<U> Class<? extends U> asSubclass(Class<U> 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<? extends Annotation> annotationClass)
Annotation[] getAnnotations()
Annotation[] getDeclaredAnnotations()
<A extends Annotation> A getAnnotation(Class<A> 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<? extends Annotation> annotationClass)
Annotation[] getAnnotations()
Annotation[] getDeclaredAnnotations()
<A extends Annotation> A getAnnotation(Class<A> 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<T>
La classe Constructor<T> è 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<T> 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<T>:

String getName()
int getModifiers()
Class<?>[] getParameterTypes()
Type[] getGenericParameterTypes()
Class<?>[] getExceptionTypes()
Type[] getGenericExceptionTypes()
TypeVariable<Constructor<T>>[] 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<? extends Annotation> annotationClass)
Annotation[] getAnnotations()
Annotation[] getDeclaredAnnotations()
<A extends Annotation> A getAnnotation(Class<A> 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<Method>[] 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<T>:

boolean isAnnotationPresent(Class<? extends Annotation> annotationClass)
Annotation[] getAnnotations()
Annotation[] getDeclaredAnnotations()
<A extends Annotation> A getAnnotation(Class<A> 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<? extends Annotation> 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<? extends Annotation> annotationClass)
<A extends Annotation> A getAnnotation(Class<A> 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 2:


Figura 2
- Gerarchia di Type e delle sue sotto interfacce

Il diagramma delle classi di Type è molto complesso, e riflette la complessità della meta-rappresentazione 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 <D extends GenericDeclaration>
L'interfaccia TypeVariable<D extends GenericDeclaration> è 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 <?>, <? extends Number> o <? super JCompo-nent>) 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.