MokaByte 103 - Gennaio 2006
 
MokaByte 103 - Gennaio 2006 Prima pagina Cerca Home Page

 

 

 

La reflection in Java
III parte

Questo mese termina la panoramica sull’API Reflection, che permette di manipolare in modo dinamico oggetti Java a runtime. Nei prossimi paragrafi verrà mostrato come creare oggetti, leggere o scrivere attributi e invocare metodi con la reflection. Infine verranno illustrate le modalità con cui è possibile operare in modo dinamico su array

Creazione di oggetti
L’API Reflection permette di creare oggetti anche nel caso la classe base non sia conosciuta nel momento in cui il programma viene scritto. Il sistema più rapido per creare un oggetto a partire da una classe è il metodo newInstance() di Class:

public T newInstance()

Questo metodo restituisce un oggetto di tipo T (il tipo base della classe stessa) creato invocando il costruttore privo di parametri. Se la classe corrispondente non possiede un costruttore vuoto, viene generata una a NoSuchMethodException. Il seguente frammento di codice mostra come caricare un oggetto Class di tipo “java.lang.String” e crearne un’istanza:

try {
Class<String> c = (Class<String>)ClassLoader.getSystemClassLoader().loadClass("java.lang.String");
String s = c.newInstance();
}
catch (Exception e) {
e.printStackTrace();
}

Per creare oggetti attraverso un costruttore dotato di parametri, è necessario operare su un oggetto di tipo Constructor. I passaggi necessari per completare l’operazione sono tre:

  • Prendere un oggetto Class relativo al tipo di oggetti che si desidera creare
  • Creare un oggetto Constructor attraverso l’invocazione dell’apposito metodo getConstructor() sull’oggetto Class. Il metodo getConstructor() richede come parametro un array di Class che deve corrispondere ai tipi richiesti dal costruttore desiderato
  • Creare l’oggetto desiderato attraverso l’invocazione del metodo newInstance() sull’oggetto Constructor appena ottenuto. Il metodo newInstance() richiede come parametro un array di Object che deve contenere i parametri da passare al costruttore.

Si osservi che i parametri appartenenti a tipi primitivi devono essere specificati attraverso le classi dei corrispondenti Wrapper Type (Integer, Float, Boolean ecc). Un esempio dovrebbe chiarire la situazione. Per replicare con la reflection la seguente istruzione:

Vector v = new Vector(10,5);

È necessario utilizzare il seguente frammento di codice:

try {
  Class<Vector> c = (Class<Vector>)ClassLoader.getSystemClassLoader()
                                              .loadClass("java.util.Vector");
  Constructor<Vector> constructor = c.getConstructor(new Class[] { Integer.class,
                                                                                         Integer.class });
  Vector v = constructor.newInstance(new Object[] { new Integer(10),
                                                          new Integer(5) });
  }
  catch (Exception e) {
    e.printStackTrace();
}

La prima riga carica la classe “java.util.Vector” dal ClassLoader; la seconda recupera il costruttore che opera su due parametri interi; la terza invoca il costruttore sulla coppia di parametri 10 e 5, racchiusi negli appositi wrapper type.
Un’applicazione che debba realmente ricorrere alla reflection per creare oggetti a runtime deve verosimilmente mostrare un elenco di costruttori e permettere all’utente di scegliere quello da invocare. L’interrogazione di una classe al fine di ottenere l’elenco di costruttori, metodi e attributi è stata illustrata nell’articolo del mese scorso.

 

Lettura e scrittura di attributi
La Reflection permette l’accesso dinamico agli attributi di un oggetto: in sostanza è possibile interrogare un oggetto senza conoscerne a priori la classe di appartenenza. Anche in questo caso si tratta di un’operazione caratterizzata da tre passaggi:
Creare un oggetto Class relativo alla classe dell’oggetto da esaminare
Creare un oggetto Field corrispondente al campo da esaminare con il metodo getField(String) di Class
Recuperare il valore del campo con il metodo get(Object), che richede come parametro l’istanza da interrogare.

La classe Field dispone anche di metodi specializzati per recuperare valori appartenenti a tipi primitivi (getInt(), getFloat() ecc). Questi ultimi sono metodi di convenienza: è sempre possibile recuperare valori primitivi incapsulati negli appositi oggetti Wrapper con il consueto metodo get(Object). Si noti tuttavia che, qualora l’attributo contenga un array di primitivi, il valore restituito sarà un array di primitivi, non un array di wrapper type. Questo argomento verrà chiarito ulteriormente nel paragrafo sulla manipolazione di array.

Nel caso si desideri leggere un attributo statico, l’argomento della get(Object) deve essere null. Il seguente frammento di codice mostra come fare per leggere l’attributo statico PI (pigreco) dalla classe java.lang.Math:

try {
  Class<java.lang.Math> c = (Class<java.lang.Math>)ClassLoader.getSystemClassLoader()
                                                                               .loadClass("java.lang.Math");
  Field f = c.getField("PI");
  System.out.println(f.get(null));
}
catch (Exception e) {
  e.printStackTrace();
}

In maniera del tutto speculare esiste un metodo che permette di modificare il valore degli attributi di un determinato oggetto:

void set(Object obj, Object value)

il metodo set() richede come parametri l’oggetto su cui effettuare l’operazione e il valore da impostare. Anche in questo caso esiste una famiglia di metodi di comodo per manipolare valori primitivi (setInteger(Object,int), setFloat(Object,float) e così via).

 

Invocazione di metodi
La reflection permette anche di invocare metodi in modo dinamico. I passaggi necessari sono simili a quelli già visti nel caso dei costruttori:

  • Prendere un oggetto Class relativo alla famiglia di oggetti che si desidera creare
  • Creare un oggetto Method attraverso l’invocazione dell’apposito metodo getMethod(String m,Class[] parameterTypes) sull’oggetto Class. Questo metodo richede come parametri il nome del metodo e un array di Class che deve corrispondere ai tipi richiesti dal metodo desiderato. Se si desidera ottenere un metodo privo di parametri, occorre passare un vettore di lunghezza zero o null.
  • Invocare il metodo attraverso la invoke(Object o,Object[] args) a cui bisogna passare l’oggetto su cui si vuole eseguire l’operazione e il vettore degli argomenti (null o un vettore di lunghezza zero se si desidera invocare un metodo privo di parametri). La invoke() restituisce sotto forma di Object il valore di ritorno della chiamata. Nel caso si tratti di un valore primitivo, questo sarà contenuto all’interno di un oggetto wrapper.

Nell’esempio seguente viene creato un oggetto Vector con alcune stringhe al suo interno; quindi viene invocato il metodo “elements” con la reflection. La chiamata non richiede parametri, pertanto sia la getMethod() che la invoke() vengono chiamate con un vettore di lunghezza zero. Il valore di ritorno viene convertito in una Enumeration grazie all’operatore di casting, e quindi viene eseguita una normale operazione di iterazione tra gli elementi in esso contenuti:

Vector v = new Vector();
v.add("primo");
v.add("secondo");
v.add("terzo");
try {
  Class c = v.getClass();
  Method m = c.getMethod("elements",new Class[0]);
  Enumeration elements = (Enumeration)m.invoke(v,new Object[0]);
  while(elements.hasMoreElements())
    System.out.println(elements.nextElement());
  }
catch (Exception e) {
  
e.printStackTrace();
}

 

Manipolazione di array
La classe Array, presente nel package java.lang.reflect, dispone di una serie di metodi statici che permettono di manipolare array. Per creare un array sono disponibili due versioni del metodo newInstance:

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

Il primo metodo permette di creare array monodimensionali specificando il tipo e la lunghezza; il secondo restituisce array multidimensionali sulla base di un vettore di interi in cui ciascun elemento denota una dimensione. In pratica l’istruzione

int[] array = (int[])Array.newInstance(int.class,5)

è equivalente ad una

int[] array = new int[5];

mentre l’istruzione

int[][] array = (int[][])Array.newInstance(int.class,new int[]{3,4});

equivale alla direttiva

int[][] array = new int[3][4]

Nel definire array è necessario sottolineare che i vettori di tipi primitivi sono diversi da vettori di wrapper type. Si osservino i due esempi seguenti:

int[] array1 = (int[])Array.newInstance(int.class,5);
Integer[] array2 = (Integer[])Array.newInstance(Integer.class,5);

Il primo crea un vettore di int, il secondo un vettore di Integer. Si tratta di due tipi differenti, come del resto viene sottolineato dal primo parametro della chiamata. In nessun caso è possibile convertire l’uno nell’altro con un’operazione di casting: l’unico modo per ottenere un vettore di int a partire da un vettore di Integer (o viceversa) è quello di creare un nuovo vettore e copiare gli elementi uno ad uno, effettuando l’opportuna correzione su ogni elemento.

La classe Array dispone di una serie di metodi, già illustrati nell’articolo di novembre, per interrogare o modificare gli elementi di un array. Il seguente frammento di codice definisce un metodo che, preso in input un vettore qualsiasi, ne restituisce uno dello stesso tipo ma di lunghezza doppia, che contiene nelle prime posizioni tutti gli elementi del vettore in ingresso. Questa breve procedura mostra tutte le modalità di interazione con gli array tramite la reflection:

public static Object raddoppiaArray(Object source) {
  if(!source.getClass().isArray())
  
  throw new IllegalArgumentException("Il parametro deve essere un array");
  int sourceLength = Array.getLength(source);
  Class componentClass = source.getClass().getComponentType();
  Object result = Array.newInstance(componentClass, sourceLength * 2);
  for(int i=0;i<sourceLength;i++)
    Array.set(result,i,Array.get(source,i));
return result;
}

La prima istruzione verifica che il parametro source sia un array, e in caso contrario genera un’eccezione. Quindi l’array viene interrogato per con il metodo Array.getLength(). L’istruzione successiva mostra come ottenere il tipo degli elementi contenuti nell’array, che è diverso dal tipo dell’array (ad esempio se il primo è int, il secondo sarà int[]). Quindi viene creato un vettore con il metodo Array.newInstance(). Infine attraverso un ciclo, gli elementi dell’array source vengono copiati nell’array result.

 

Conclusioni
Questo mese abbiamo completato la panoramica sull’API reflection, che permette di manipolare in modo dinamico gli oggetti Java. Per prima cosa abbiamo visto come creare istanze di oggetti, quindi come si leggono e scrivono gli attributi e come si invocano i metodi. Infine abbiamo illustrato come si creano e manipolano gli Array. Queste conoscenze, unite a quelle presenti nei due articoli precedenti, permettono di realizzare procedure che operano su oggetti Java anche nei casi in cui la classe base non è nota al momento dello sviluppo.