MokaByte 102 - Dicembre 2005
 
MokaByte 102 - Dicembre 2005 Prima pagina Cerca Home Page

 

 

 

La reflection in Java
II parte

Come già spiegato il mese scorso, la reflection fornisce un meccanismo per ispezionare le classi in fase di esecuzione, senza disporre di alcuna informazione su di esse al momento della stesura del programma. Per questa ragione, la reflection si classifica come strumento ideale per la realizzazione di strumenti per la diagnostica o il debug del software, più che come strumento di programmazione vero e proprio.

Come ispezionare le classi
Per ogni classe, la JVM mantiene un oggetto Class che contiene tutte le informazioni sulla classe stessa in termini di composizione. Il mese scorso sono state analizzate le API reflection, e si è visto come a partire da un oggetto Class sia possibile ottenere l’elenco di campi, metodi, costruttori e così via, laddove ciascun elemento del linguaggio è a sua volta rappresentato da un’apposita classe (Field, Constructor, Method e così via). Ciascuno di questi oggetti può a sua volta fornire un insieme completo di informazioni sull’elemento sottostante. E’ giunto il momento di analizzare codice funzionante che esegua le operazioni descritte fino ad ora.

 

Come recuperare gli oggetti Class
Esistono tre sistemi per recuperare oggetti Class. Il primo è attraverso il metodo statico forName() presente nella classe Class stessa. In questo caso l’unico parametro da fornire è il nome della classe sotto forma di stringa, un’informazione che può essere fornita tranquillamente a runtime.

import java.io.*;

public class ClassBuster1 {
public static void main(String argv[]) {
try {
BufferedReader lineReader = new BufferedReader(new InputStreamReader(System.in));
System.out.println("Inserire il nome completo di una classe: ");
String className = lineReader.readLine();
Class c = Class.forName(className);
System.out.println("La classe cercata e': " + c);
}
catch (ClassNotFoundException e) {
System.out.println("La classe richiesta non e' stata trovata");
e.printStackTrace();
}
catch (IOException ioe) {
ioe.printStackTrace();
}
}
}

Lanciando questo programma viene richiesto all’utente di inserire il nome di una classe. Quindi il programma cerca la classe inserita e se la trova stampa informazioni sulla classe trovata e sulle sue superclassi:

C:\>java ClassBuster1
Inserire il nome completo di una classe:
javax.swing.JTable
La classe cercata e': class javax.swing.JTable
La superclasse numero 1 e': class javax.swing.JTable
La superclasse numero 2 e': class javax.swing.JComponent
La superclasse numero 3 e': class java.awt.Container
La superclasse numero 4 e': class java.awt.Component
La superclasse numero 5 e': class java.lang.Object

Se la classe non viene trovata, viene stampato a schermo un messaggio di errore e lo stack del programma al momento dell’esecuzione.

Se si dispone di un’istanza di un generico oggetto Object, è possibile chiamare l’apposito metodo getClass():

public static Class getClass(Object o) {
return o.getClass();
}

Infine, se si conosce il nome di una classe al momento della realizzazione del programma, è possibile recuperare il relativo oggetto Class aggiungendo il suffisso .class al suo nome:

Class c = java.util.Vector.class;

 

Distinguere la natura dell’oggetto Class
La classe Class dispone di un buon numero di metodi che permettono di riconoscere la natura dell’oggetto Class stesso. Il prossimo esempio è una variante del programma precedente, che dopo aver chiesto il nome di una classe, cerca di verificare se si tratta di un’interfaccia, di una classe interna o di una classe:

import java.io.*;

public class ClassBuster2 {

  public static void main(String argv[]) {
    try {
      BufferedReader lineReader = new BufferedReader(new InputStreamReader(System.in));
      System.out.println("Inserire il nome completo di una classe: ");
      String className = lineReader.readLine();
      Class c = Class.forName(className);
      if(c.isInterface())
        System.out.println("L'interfaccia cercata e': " + c);
      else if(c.isMemberClass())
        System.out.println("La classe interna cercata e': " + c);
      else
        System.out.println("La classe cercata e': " + c);
    }
    catch (ClassNotFoundException e) {
      System.out.println("La classe richiesta non e' stata trovata");
      e.printStackTrace();
    }
    catch (IOException ioe) {
      ioe.printStackTrace();
    }
  }
}

Si provi ad inserire il nome di una classe, di un’interfaccia come java.util.List, o di una classe interna come javax.swing.JTable$PrintMode. Esistono metodi per verificare se l’oggetto Class rappresenta un array, una enum o altre cose, ma non è stato possibile inserirle nell’esempio precedente: il loro funzionamento comunque dovrebbe oramai risultare chiaro.

 

Identificare i modificatori
Una feature importante quando si usa la reflection è la capacità di identificare i modificatori. Come già spiegato nel precedente articolo, i modificatori di classi, metodi, campi e costruttori vengono riportati sotto forma di un intero che deve poi essere processato con l’apposito oggetto java.lang.reflect.Modifier. Nel seguente esempio vedremo come

import java.io.*;
import java.lang.reflect.*;

public class ClassBuster3 {

  public static void main(String argv[]) {
    try {
      BufferedReader lineReader = new BufferedReader(new InputStreamReader(System.in));
      System.out.println("Inserire il nome completo di una classe: ");
      String className = lineReader.readLine();
      Class c = Class.forName(className);
      System.out.println("L'oggetto Class ha i seguenti modificatori:
                         "+Modifier.toString(c.getModifiers()));
    }
    catch (ClassNotFoundException e) {
      System.out.println("La classe richiesta non e' stata trovata");
      e.printStackTrace();
    }
    catch (IOException ioe) {
      ioe.printStackTrace();
    }
  }
}

Un esempio di utilizzo della classe precedente:

C:\>java ClassBuster3
Inserire il nome completo di una classe:
java.util.AbstractList
L'oggetto Class ha i seguenti modificatori: public abstract

Si provi ad usare il programma anche su classi differenti, come java.lang.String, che è public e final. Il meccanismo appena individuato opera altrettanto bene su qualunque altro elemento di una classe, come sarà possibile vedere più avanti.
Identificare le interfacce implementate da una classe
Si è già visto nel primo esempio come sia possibile scoprire la superclasse di un oggetto Class. Scoprire le sue interfacce è altrettanto semplice: basta usare l’apposito metodo getInterfaces(), come si può vedere nel prossimo esempio:

import java.io.*;
import java.lang.reflect.*;

public class ClassBuster4 {

  public static void main(String argv[]) {
    try {
      BufferedReader lineReader = new BufferedReader(new InputStreamReader(System.in));
      System.out.println("Inserire il nome completo di una classe: ");
      String className = lineReader.readLine();
      Class c = Class.forName(className);
      Class interfaces[] = c.getInterfaces();
      System.out.printf("La classe %s implementa le seguenti interfacce:%n",c);
      for(Class i : interfaces)
        System.out.printf("%s %s%n",Modifier.toString(i.getModifiers()),i);
      }
    catch (ClassNotFoundException e) {
      System.out.println("La classe richiesta non e' stata trovata");
      e.printStackTrace();
    }  
    catch (IOException ioe) {
      ioe.printStackTrace();
    }
  }
}

Si provi ad eseguire il precedente programma su una classe che implementa un gran numero di interfacce, come la classe java.util.Vector:

C:\>java ClassBuster4
Inserire il nome completo di una classe:
public abstract interface interface java.util.List
public abstract interface interface java.util.RandomAccess
public abstract interface interface java.lang.Cloneable
public abstract interface interface java.io.Serializable

 

Ispezionare campi, costruttori e metodi
Come ultimo esempio, un complete ispezionatore di classi in grado di fornire informazioni su tutti gli elementi della classe, compresi campi, metodi e costruttori. Tutto inizia con un metodo main() che richiede il nome della classe da visitare:

import java.io.*;
import java.lang.reflect.*;
import java.lang.annotation.*;

public class ClassVisitor {

  public static void main(String argv[]) throws Exception {
    BufferedReader lineReader = new BufferedReader(new InputStreamReader(System.in));
    System.out.println("Inserire il nome completo di una classe: ");
    String className = lineReader.readLine();
    Class c = Class.forName(className);
    visit(c);  
  }
}


Il metodo visit(Class c) deve anzitutto verificare se il presente oggetto Class rappresenta una classe o un’interfaccia:

  public static void visit(Class c) {
  
  if(c.isInterface())
  
    visitInterface(c);
    else
  
    visitClass(c);
  }

Il metodo che ispeziona le classi deve anzitutto raccogliere informazioni sui fodificatori, quindi sulle annotazioni, con un apposito metodo parseAnnotatedElement(), quindi aggiunge la stringa “class”, il nome della classe e le eventuali dichiarazioni generiche, attraverso un apposito metodo parseGenericDeclaration(). Segue la raccolta di informazioni sulla superclasse e sulle interfacce implementate. La stringa così creata viene stampata attraverso un apposito metodo print, che effettua una stampa formattata, e passa ad esplorare il contenuto della classe attraverso l’apposito metodo visitClassOrInterfaceDeclarations():

public static void visitClass(Class c) {
  String description = Modifier.toString(c.getModifiers())+" ";
  description = description + parseAnnotatedElement(c);
  description = description + " class " + c.getSimpleName() + parseGenericDeclaration(c)+" ";
  Class superclass = c.getSuperclass();
  if(superclass != null) {
  
  description = description + "extends " + superclass.getSimpleName();
  
  description = description + parseGenericDeclaration(superclass)+" ";
  }
  Class[] interfaces = c.getInterfaces();
  if(interfaces.length!=0) {
  
  description = description + "implements ";
  
  for(Class i : interfaces) {
  
    description = description + i.getSimpleName();
  
    description = description + parseGenericDeclaration(i) + ",";
  
  }
  
  description = description.substring(0,description.length()-1);
  }  
  print(description);
  visitClassOrInterfaceDeclarations(c);
}

Il metodo visitInterface(Class c) assomiglia molto al precedente, con la differenza che il metodo getInterfaces() restituisce le superinterfacce, che vanno pertanto precedute dalla parola chiave “extends”. Anche in questo caso la successiva ispezione viene affidata all’apposito metodo visitClassOrInterfaceDeclarations():

public static void visitInterface(Class c) {
  String description = Modifier.toString(c.getModifiers())+" ";
  description = description + parseAnnotatedElement(c);
  description = description + "interface" + parseGenericDeclaration(c) +
  
              c.getSimpleName() + " ";
  
Class[] interfaces = c.getInterfaces();
  
if(interfaces.length!=0) {
  
  description = description + "extends ";
  
  for(Class i : interfaces) {
  
    description = description + i.getSimpleName();
  
    description = description + parseGenericDeclaration(i) + ",";
  
  }
  
  description = description.substring(0,description.length()-1);
  }
  print(description);
  visitClassOrInterfaceDeclarations(c);
}

Il metodo visitClassOrInterfaceDeclarations() chiama il metodo indent(), che opera una indentazione di un paio di spazi, e quindi, attraverso i metodi getDeclaredFields(), getDeclaredConstructors(), getDeclaredMethod() e getDeclaredClasses() passa in ispezione con appositi metodi gli elementi dichiarati all’interno dell’oggetto Class:

public static void visitClassOrInterfaceDeclarations(Class c) {
  indent();
  Field[] fields = c.getDeclaredFields();
  for(Field f : fields)
  visit(f);
  Constructor[] constructors = c.getDeclaredConstructors();
  for(Constructor constructor : constructors)
  
  visit(constructor);
  
  Method[] methods = c.getDeclaredMethods();
  
  for(Method m : methods)
  
  visit(m);
  
  Class[] classes = c.getDeclaredClasses();
  
  for(Class cl : classes)
  
    visit(cl);
  
  deindent();
}

Il metodo visit(Field f) raccoglie informazioni sui modificatori, sulle annotazioni, sul tipo (generico o meno) e infine sul nome. Quindi invia la stringa in stampa:

public static void visit(Field f) {
  String description = Modifier.toString(f.getModifiers())+" ";
  description = description + parseAnnotatedElement(f);
  description = description +(Object)f.getGenericType();
  description = description +" " +f.getName();
  print(description);
}

Il metodo visitConstructor(Constructor c) è molto più complesso del precedente, dal momento che deve anche raccogliere informazioni su eventuali dichiarazioni generiche e dei parametri, che a loro volta possono essere generici. Infine non vanno dimenticate le eccezioni, che vengono raccolte con l’apposito metodo getExceptionTypes():

public static void visit(Constructor c) {
  String description = Modifier.toString(c.getModifiers())+" ";
  description = description + parseAnnotatedElement(c);
  description = description + parseGenericDeclaration(c);
  description = description + c.getName() + "(";
  Class[] parameters = c.getParameterTypes();
  if(parameters.length!=0) {
  
  for(Class p : parameters)
  
    description = description + p.getSimpleName()+parseGenericDeclaration(p)+",";
    description = description.substring(0,description.length()-1);
  
}
  description = description + ")";
  Class[] exceptions = c.getExceptionTypes();
  if(exceptions.length != 0) {
  
  description = description + " throws ";
  
  for(Class e : exceptions)
  
    description = description + e.getSimpleName() +parseGenericDeclaration(e)+",";
  
  description = description.substring(0,description.length()-1);
  }
  print(description);
}

Il metodo visit(Method m) è simile al precedente, con la differenza che un metodo deve avere un tipo di ritorno che viene cercato col metodo m.getReturnType().getSimpleName(), seguito da parseGenericDeclaration(m) che permette di catturare eventuali dichiarazioni generiche nel tipo di ritorno:

public static void visit(Method m) {
  String description = Modifier.toString(m.getModifiers())+" ";
  description = description + parseAnnotatedElement(m);
  description = description + m.getReturnType().getSimpleName() +
  parseGenericDeclaration(m) + " ";
  TypeVariable[] typeVariables = m.getTypeParameters();
  for(TypeVariable typeVariable : typeVariables)
  
  description = description + typeVariable.getName() +" ";
  
  description = description + m.getName() +"(";
  
  Class[] parameters = m.getParameterTypes();
  
  if(parameters.length!=0) {
  
    for(Class p : parameters)
  
      description = description + p.getSimpleName()+parseGenericDeclaration(p)+",";
  
      description = description.substring(0,description.length()-1);
  
    }
  
  description = description + ") ";
  
  Class[] exceptions = m.getExceptionTypes();
  
  if(exceptions.length != 0) {
  
    description = description + "throws ";
  
    for(Class e : exceptions)
  
      description = description + e.getSimpleName() +parseGenericDeclaration(e) +",";
  
      description = description.substring(0,description.length()-1);
  
    }
  
  print(description);
}

Il metodo parseAnnotatedElement() restituisce una stringa contenente le eventuali annotazioni presenti nella classe, nel campo, nel costruttore o nel metodo specificato:

protected static String parseAnnotatedElement(AnnotatedElement annotatedElement) {
  Annotation[] annotations = annotatedElement.getAnnotations();
  String description = "";
  for(Annotation a : annotations)
  
  description = description + a + " ";
  
  return description;
}

Infine il metodo parseGenericDeclaration() ricostruisce le stringhe che denotano tipi parametrici, tipo <T> o <K,V>:

protected static String parseGenericDeclaration(GenericDeclaration genericDeclaration) {
  TypeVariable[] typeVariables = genericDeclaration.getTypeParameters();
  String description = "";
  if(typeVariables.length!=0) {
  
  description = "<";
  
  for(TypeVariable t : typeVariables) {
  
    description = description + t.getName();
  
    description = description + ",";
  
  }
  
  description = description.substring(0,description.length()-1);
  
  description = description + ">";
  }
  return description;
}

Data la complessità dell’esempio, è meglio presentarne il sorgente completo:

import java.io.*;
import java.lang.reflect.*;
import java.lang.annotation.*;

public class ClassVisitor {

  private static int spaces = 0;

  protected static void indent() {
    spaces++;
  }
  protected static void deindent() {
    spaces --;
  }
  protected static void print(String s) {
    for(int i=0 ; i<spaces ; i++)
      System.out.print(" ");
    System.out.println(s);
  }
  public static void visit(Class c) {
    if(c.isInterface())
      visitInterface(c);
    else
      visitClass(c);
  }

  public static void visitClass(Class c) {
    String description = Modifier.toString(c.getModifiers())+" ";
    description = description + parseAnnotatedElement(c);
    description = description + " class " + c.getSimpleName() + parseGenericDeclaration(c)+" ";
    Class superclass = c.getSuperclass();
    if(superclass != null) {
      description = description + "extends " + superclass.getSimpleName();
      description = description + parseGenericDeclaration(superclass)+" ";
    }
    
Class[] interfaces = c.getInterfaces();
    if(interfaces.length!=0) {
      description = description + "implements ";
      for(Class i : interfaces) {
        description = description + i.getSimpleName();
        description = description + parseGenericDeclaration(i) + ",";
      }
      description = description.substring(0,description.length()-1);
    }
    print(description);
    visitClassOrInterfaceDeclarations(c);
  }
  public static void visitInterface(Class c) {
    String description = Modifier.toString(c.getModifiers())+" ";
    description = description + parseAnnotatedElement(c);
    description = description + "interface" +
                  parseGenericDeclaration(c) + c.getSimpleName() + " ";
    Class[] interfaces = c.getInterfaces();
    if(interfaces.length!=0) {
      description = description + "extends ";
      for(Class i : interfaces) {
        description = description + i.getSimpleName();
        description = description + parseGenericDeclaration(i) + ",";
      }
      description = description.substring(0,description.length()-1);
    }
    print(description);
    visitClassOrInterfaceDeclarations(c);
  }

  public static void visitClassOrInterfaceDeclarations(Class c) {
    indent();
    Field[] fields = c.getDeclaredFields();
    for(Field f : fields)
      visit(f);
    Constructor[] constructors = c.getDeclaredConstructors();
    for(Constructor constructor : constructors)
      visit(constructor);
    Method[] methods = c.getDeclaredMethods();
    for(Method m : methods)
      visit(m);
    Class[] classes = c.getDeclaredClasses();
    for(Class cl : classes)
      visit(cl);
    deindent();
  }

  public static void visit(Constructor c) {
    String description = Modifier.toString(c.getModifiers())+" ";
    description = description + parseAnnotatedElement(c);
    description = description + parseGenericDeclaration(c);
    description = description + c.getName() + "(";
    Class[] parameters = c.getParameterTypes();
    if(parameters.length!=0) {
      for(Class p : parameters)
        description = description + p.getSimpleName()+parseGenericDeclaration(p)+",";
        description = description.substring(0,description.length()-1);
      }
    description = description + ")";
    Class[] exceptions = c.getExceptionTypes();
    if(exceptions.length != 0) {
      description = description + " throws ";
      for(Class e : exceptions)
        description = description + e.getSimpleName() +parseGenericDeclaration(e)+",";
        description = description.substring(0,description.length()-1);
    }
    print(description);
  }

  public static void visit(Field f) {
    String description = Modifier.toString(f.getModifiers())+" ";
    description = description + parseAnnotatedElement(f);
    description = description +(Object)f.getGenericType();
    description = description +" " +f.getName();
    print(description);
  }

  public static void visit(Method m) {
    String description = Modifier.toString(m.getModifiers())+" ";
    description = description + parseAnnotatedElement(m);
    description = description + m.getReturnType().getSimpleName() +
                  parseGenericDeclaration(m) + " ";
    TypeVariable[] typeVariables = m.getTypeParameters();
    for(TypeVariable typeVariable : typeVariables)
      description = description + typeVariable.getName() +" ";
    description = description + m.getName() +"(";
    Class[] parameters = m.getParameterTypes();
    if(parameters.length!=0) {
      for(Class p : parameters)
        description = description + p.getSimpleName()+parseGenericDeclaration(p)+",";
    description = description.substring(0,description.length()-1);
  }
  description = description + ") ";
  Class[] exceptions = m.getExceptionTypes();
  if(exceptions.length != 0) {
    description = description + "throws ";
  for(Class e : exceptions)
    description = description + e.getSimpleName() +parseGenericDeclaration(e) +",";
    description = description.substring(0,description.length()-1);
  }
  print(description);
  }

  protected static String parseAnnotatedElement(AnnotatedElement annotatedElement) {
    Annotation[] annotations = annotatedElement.getAnnotations();
    String description = "";
    for(Annotation a : annotations)
      description = description + a + " ";
      return description;
  }

  protected static String parseGenericDeclaration(GenericDeclaration genericDeclaration) {
    TypeVariable[] typeVariables = genericDeclaration.getTypeParameters();
    String description = "";
    if(typeVariables.length!=0) {
      description = "<";
      for(TypeVariable t : typeVariables) {
        description = description + t.getName();
        description = description + ",";
      }
      description = description.substring(0,description.length()-1);
      description = description + ">";
    }
    return description;
  }

  public static void main(String argv[]) throws Exception {
    BufferedReader lineReader = new BufferedReader(new InputStreamReader(System.in));
    System.out.println("Inserire il nome completo di una classe: ");
    String className = lineReader.readLine();
    Class c = Class.forName(className);
    visit(c);
  }
}

Al momento dell’esecuzione viene richiesto di specificare una classe: si provi con classi contenenti annotazioni @deprecated, tipo java.lang.String, o classi contenenti tipi parametrici, come java.util.Vector, java.util.List o java.util.Map.

 

Conclusioni
Questo mese è stato analizzato l’uso pratico dell’API reflection, attraverso una serie di esempi che ha permesso di studiare in modo esaustivo le potenzialità dell’API stessa. Il mese prossimo verranno approfondite ulteriormente le API al fine di comprendere i meccanismi attraverso i quali la reflection permette di manipolare gli oggetti.

Allegati
Scarica qui i sorgenti presentati nell'articolo