Metodologie di Programmazione: Lezione 24

Riccardo Silvestri

Reflection

In quest'ultima lezione vedremo un'introduzione alla cosiddetta reflection. Questo è uno strumento che permette di ottenere informazioni al runtime che potremmo dire "introspettive" sulle classi, le interfacce e gli oggetti che aggirano i vincoli imposti dalle regole di accessibilità del linguaggio. La reflection è sopratutto usata per realizzare strumenti per la costruzione automatica di codice come ad esempio per facilitare lo unit testing o la costruzione di UI. È usata da alcune parti della libreria di Java, ad esempio per la serializabilità.

Anatomia di una classe

Come primo esempio di utilizzo della reflection, definiamo alcuni metodi che permettono di ottenere informazioni circa tutti i membri di una classe o interfaccia. Più precisamente vogliamo ottenere una stringa stampabile con le intestazioni dei membri di una classe data, quindi costruttori, metodi, campi e anche classi e interfacce annidate. Il package java.lang.reflect offre un framework per ottenere tali informazioni e molte altre relativamente a classi, interfacce e annotazioni durante l'esecuzione del programma.

Prima di tutto per specificare la classe o interfaccia su cui vogliamo avere le informazioni basta dare una stringa con il nome completo, cioè il nome comprensivo del package, ad es. "java.lang.String". Il punto di partenza per ottenere le informazioni è l'oggetto Class relativo alla classe (o interfaccia) voluta. Questo si ottiene tramite il metodo statico Class<?> forName(String cName) di Class. Se non riesce a trovarla, lancia l'eccezione ClassNotFoundException. Una volta ottenuto l'oggetto Class della classe (o interfaccia), per ogni caratteristica della classe ci sono vari metodi che ritornano informazioni su quella caratteristica. Questi li spiegheremo dopo aver visto il codice.

Introduciamo un nuovo package mp.reflect e in esso definiamo la classe Utils nella quale definiamo una prima versione dei metodi.

package mp.reflect;

import java.lang.reflect.*;

/** Metodi di utilità basati sulla reflection */
public class Utils {
    public static final boolean SIMPLE_NAMES = true;

    /** Equivalente a {@link Utils#classToString(Class, String)
     * classToString(Class.forName(cName),"")}.
     * @param cName  il nome completo di una classe/interfaccia
     * @return una stringa con le intestazioni di tutti i membri della classe
     * @throws ClassNotFoundException se la classe non esiste */
    public static String classToString(String cName) 
                                       throws ClassNotFoundException {
        return classToString(Class.forName(cName), "");
    }

    /** Ritorna una stringa con le intestazioni di tutti i membri della classe
     * (o interfaccia) specificata.
     * @param c  una classe o interfaccia
     * @param indent  l'indentazione iniziale delle linee della stringa
     * @return una stringa con le intestazioni di tutti i membri della classe */
    public static String classToString(Class<?> c, String indent) {
        String s = "";
        String ind = indent+"\t";
        try {
            s += indent+c+" {\n";                   // Intestazione della classe
            for (Class<?> ec : c.getDeclaredClasses())    // Classi annidate
               s += classToString(ec, ind); 
            for (Constructor<?> co : c.getDeclaredConstructors())  // Costruttori
                s += ind+co.toGenericString()+"\n";   // Intestazione costruttore
            s += "\n";
            for (Method m : c.getDeclaredMethods())       // Metodi
                s += ind+m.toGenericString()+"\n";    // Intestazione metodo
            s += "\n";
            for (Field f : c.getDeclaredFields())         // Campi
                s += ind+f.toGenericString()+"\n";    // Intestazione campo
            s += indent+"}\n";
        } catch(Exception e) { s += "\nERROR: "+e; }
        return s;
    }
}

Il metodo che prende in input una stringa con il nome completo della classe/interfaccia invoca un metodo ausiliario che prende in input l'oggetto Class corrispondente. Questo per facilitare l'invocazione ricorsiva relativamente alle classi/interfacce annidate.

Per l'intestazione della classe/interfaccia ci si può accontentare, per adesso, di quella prodotta da toString() dell'oggetto Class. Per ogni eventuale classe/interfaccia annidata, ottenute con il metodo getDeclaredClasses(), facciamo un'invocazione ricorsiva del metodo. I costruttori si possono ottenere con getDeclaredConstructors() che ritorna un array di Constructor<?>1. Un oggetto Constructor<?> rappresenta appunto un costruttore e permette, come vedremo fra poco, di ottenere varie informazioni su di esso. Per ora ci limitiamo a prendere la stringa ritornata da toGenericString() che a differenza del semplice toString() include anche le eventuali variabili di tipo. In modo analogo otteniamo le informazioni sui metodi con getDeclaredMethods() che ritorna un array di Method. Come si intuisce un oggetto Method rappresenta un metodo e anche per loro usiamo solamente toGenericString() che è del tutto simile all'omonimo metodo di Constructor. Infine i campi si possono ottenere con getDeclaredFields() che ritorna un array di Field. Un oggetto Field rappresenta un campo con le sue proprietà che vedremo più in dettaglio fra poco, per ora prendiamo la stringa ritornata dal suo metodo toGenericString().

Dando una scorsa ai metodi di Class si noterà che oltre ai metodi getDeclaredClasses, getDeclaredConstructors, getDeclaredMethods e getDeclaredFields ci sono getClasses, getConstructors, getMethods e getFields. La differenza è che i primi ritornano tutti i membri dichiarati nella classe/interfaccia con qualsiasi livello di accessibilità, public, protected, default (cioè package access) e private esclusi quelli ereditati, mentre i secondi ritornano tutti i membri pubblici inclusi quelli ereditati, ma non altri.

Introduciamo anche una classe Tests in mp.reflect per fare dei test,

package mp.reflect;

import static java.lang.System.out;
import static reflect.Utils.*;

/** Classe per testare metodi e classi basati sulla reflection */
public class Tests {
    public static void main(String[] args) throws ClassNotFoundException {
        out.println(classToString("java.util.ArrayList"));
    }
}

Eseguendolo

class java.util.ArrayList {
    . . .
    class java.util.ArrayList$SubList {
        java.util.ArrayList$SubList(java.util.AbstractList<E>,int,int,int)

        public void java.util.ArrayList$SubList.add(int,E)
        public E java.util.ArrayList$SubList.remove(int)
        . . .
        protected void java.util.ArrayList$SubList.removeRange(int,int)
        public java.util.ListIterator<E> java.util.ArrayList$SubList.listIterator(int)
        private void java.util.ArrayList$SubList.rangeCheckForAdd(int)
        private java.lang.String java.util.ArrayList$SubList.outOfBoundsMsg(int)
        private void java.util.ArrayList$SubList.rangeCheck(int)
        private void java.util.ArrayList$SubList.checkForComodification()

        private final java.util.AbstractList<E> java.util.ArrayList$SubList.parent
        private final int java.util.ArrayList$SubList.parentOffset
        private final int java.util.ArrayList$SubList.offset
        int java.util.ArrayList$SubList.size
        final java.util.ArrayList java.util.ArrayList$SubList.this$0
    }
    . . .
    class java.util.ArrayList$Itr {
        java.util.ArrayList$Itr(java.util.ArrayList,java.util.ArrayList$1)
        private java.util.ArrayList$Itr(java.util.ArrayList)

        public void java.util.ArrayList$Itr.remove()
        final void java.util.ArrayList$Itr.checkForComodification()

        int java.util.ArrayList$Itr.cursor
        int java.util.ArrayList$Itr.lastRet
        int java.util.ArrayList$Itr.expectedModCount
        final java.util.ArrayList java.util.ArrayList$Itr.this$0
    }
    public java.util.ArrayList(java.util.Collection<? extends E>)
    public java.util.ArrayList()
    public java.util.ArrayList(int)

    . . .
    public boolean java.util.ArrayList.addAll(int,java.util.Collection<? extends E>)
    public boolean java.util.ArrayList.addAll(java.util.Collection<? extends E>)
    static int java.util.ArrayList.access$100(java.util.ArrayList)
    public void java.util.ArrayList.forEach(java.util.function.Consumer<? super E>)
    public E java.util.ArrayList.set(int,E)
    private void java.util.ArrayList.ensureCapacityInternal(int)
    E java.util.ArrayList.elementData(int)
    private void java.util.ArrayList.grow(int)
    private static int java.util.ArrayList.hugeCapacity(int)
    public boolean java.util.ArrayList.removeAll(java.util.Collection<?>)
    public boolean java.util.ArrayList.retainAll(java.util.Collection<?>)
    protected void java.util.ArrayList.removeRange(int,int)
    public java.util.ListIterator<E> java.util.ArrayList.listIterator()
    private void java.util.ArrayList.rangeCheckForAdd(int)
    private java.lang.String java.util.ArrayList.outOfBoundsMsg(int)
    private boolean java.util.ArrayList.batchRemove(java.util.Collection<?>,boolean)
    static void java.util.ArrayList.subListRangeCheck(int,int,int)

    private static final long java.util.ArrayList.serialVersionUID
    private static final int java.util.ArrayList.DEFAULT_CAPACITY
    transient java.lang.Object[] java.util.ArrayList.elementData
    private int java.util.ArrayList.size
    private static final int java.util.ArrayList.MAX_ARRAY_SIZE
}

Abbiamo tagliamo in vari punti l'output perché sarebbe stato troppo lungo. Come si può notare sono elencati membri con qualsiasi livello di accessibilità e l'ordine è arbitrario, nel senso che sono mescolati tra loro membri pubblici, privati, statici, ecc. Migliorare l'ordine è lasciato come esercizio (si veda l'esercizio [Ordine]).

Adesso vogliamo avere un maggior controllo sulle informazioni ritornate. Ad esempio per migliorare la leggibilità potremmo eliminare dai nomi dei membri e dei tipi la specifica del package. Per fare ciò non possiamo più usare il metodo getGenericString per avere la descrizione di un membro ma dobbiamo costruire la stringa esplicitamente. Ciò ci darà anche l'opportunità di vedere altri metodi degli oggetti Constructor<?>, Method e Field. Inoltre già che ci siamo vogliamo avere informazioni sulle annotazioni.

Annotazioni

Le annotazioni possono essere usate per annotare non solo metodi (ad es. @Override) e interfacce (ad es. @FunctionalInterface), come abbiamo visto finora, ma quasi tutti gli elementi di un programma Java. Quindi classi, interfacce, costruttori, metodi, parametri e campi2. Ogni annotazione è rappresentata da un oggetto Annotation nel package java.lang.annotation. Gli oggetti che rappresentano elementi che possono essere annotati hanno metodi con intestazione Annotation[] getDeclaredAnnotations() e anche Annotation[] getAnnotations(). La differenza è abbastanza sottile semplificando un po' si può dire che i primi ritornano le annotazioni direttamente presenti sull'elemento mentre i secondi ritornano anche quelle ereditate. Bisogna però precisare che non tutte le annotazioni sono visibili al runtime. Infatti un'annotazione se non esplicitamente richiesto non è visibile durante l'esecuzione. Ad esempio @Override non è visibile al runtime, d'altronde questo è naturale dato che la sua funzione si esplica durante la compilazione. Siccome le annotazioni possono essere presenti per ogni categoria di membro, conviene definire un metodo di utilità che le tratti in modo uniforme.

Modificatori

C'è anche un'altra caratteristica che è comune a tutti i membri di una classe: i modificatori. Questi si ottengono tramite metodi con intestazione sempre uguale int getModifiers(). Tali metodi ritornano i modificatori (static, public, ecc.) codificati in un int tramite opportuni flag. Per decodificarli in una stringa c'è il metodo statico toString(int mod) della classe Modifier sempre in java.lang.reflect. La classe Modifiers ha anche altri metodi statici che permettono di testare se è presente un qualsiasi modificatore, ad es. isPublic(int mod) che ritorna true se mod codifica la presenza del modificatore public.

Adesso possiamo definire il metodo che tratta annotazioni e modificatori, e nella classe Utils introduciamo,

private static String annMods(String s, Annotation[] aa, int mod) {
    for (Annotation a : aa)
        s += (s.isEmpty() ? "" : " ")+a;
    String mods = Modifier.toString(mod);
    if (!mods.isEmpty()) s += (s.isEmpty() ? "" : " ") + mods;
    return s;
}

Il metodo prende in input anche una stringa s solamente per facilitare l'integrazione delle informazioni su annotazioni e modificatori in una stringa.

Parametri di tipo

Un altro aspetto comune a vari membri sono i parametri di tipo. Infatti questi possono essere presenti nell'intestazione di classi, interfacce, metodi e costruttori. Le informazioni sugli (eventuali) parametri di tipo si ottengono tramite metodi con intestazione TypeVariable<E>[] getTypeParameters() e ritornano un array di oggetti di tipo TypeVariable<E> dove ognuno rappresenta una variabile di tipo (comprensiva di eventuali limiti, ad es. T extends Type o ? super Type). Il parametro di tipo E è il tipo del membro (ad es. Method). Possiamo trattare anche quest'informazione in modo uniforme,

private static String typeParametersToStr(String pre, TypeVariable<?>[] tp) {
    String s = "";
    if (tp.length > 0) {
        s += pre + "<";
        for (int i = 0 ; i < tp.length ; i++)
            s += (i == 0 ? "" : ",")+tp[i].toString();
        s += ">";
    }
    return s;
}

Per ogni oggetto che rappresenta un parametro di tipo nell'array di input tp ci basta prendere la descrizione riportata da toString(). Separiamo i parametri con virgole e li includiamo tra parentesi angolari. La stringa di input pre serve come sopra a integrare più facilmente la stringa in un'altra.

Ci occorre anche un altro metodo di utilità che ci permetta di produrre una stringa con solamente il nome semplice di un tipo sia generico che non generico, cioè escludendo il nome o i nomi dei package. Per questo definiamo

private static String simple(String s) {
    return s.replaceAll("\\$",".").replaceAll("[^<]*\\.", "");
}

private static String typeToStr(Type t) { return simple(t.toString()); }

Per capire l'espressione regolare del metodo simple si tenga presente che vogliamo che faccia traduzioni come le seguenti:

java.util.List<T>                   --->   List<T>
java.util.List<java.lang.String>    --->   List<String>
java.util.ArrayList$Itr             --->   Itr

Su Type, che rappresenta un qualsiasi tipo, ci torniamo fa pochissimo. Ora possiamo definire un metodo per l'intestazione di una classe o interfaccia,

public static String classToStr(Class<?> c) {
    String s = annMods("", c.getDeclaredAnnotations(), c.getModifiers());
    s += (s.isEmpty() ? "" : " ") + c.getSimpleName();
    s += typeParametersToStr("", c.getTypeParameters());
    Type sc = c.getGenericSuperclass();
    if (sc != null && sc != Object.class) s += " extends "+typeToStr(sc);
    Type[] tt = c.getGenericInterfaces();
    if (tt.length > 0) {
        s += " implements";
        for (Type t : tt)  s += " "+typeToStr(t);
    }
    return s;
}

Usa i metodi getDeclaredAnnotations() e getModifiers() per ottenere le annotazioni e i modificatori della classe/interfaccia. Il metodo getSimpleName() per ottenere il nome semplice, escluso il package. Il metodo getTypeParameters() per gli eventuali parametri di tipo. Il metodo Type getGenericSuperclass() ritorna il tipo dell'eventuale super-classe, Se non ha una super-classe, come ad es. Object o una qualsiasi interfaccia, ritorna null. L'oggetto ritornato è di tipo Type, un super-tipo di Class, che è usato per rappresentare un qualsiasi tipo. Gli oggetti Type non hanno molti metodi, c'è ne uno solo String getTypeName() che ritorna una rappresentazione tramite stringa del tipo. Se però il tipo è parametrico o generico allora il tipo specifico dell'oggetto è garantito essere uno dei seguenti quattro GenericArrayType, ParameterizedType, TypeVariable<D> o WildcardType. Quindi si potrebbe approfondire ulteriormente l'analisi del tipo, ma ci limiteremo a usare ciò che è ritornato da getTypeName(). Infine tramite il metodo Type[] getGenericInterfaces() otteniamo i tipi delle eventuali interfacce implementate o estese dalla classe/interfaccia.

Costruttori, metodi e parametri

Passiamo ora ai costruttori e ai metodi. Questi hanno in comune altri aspetti oltre a quelle già trattati. Non a caso le classi Constructor<T> e Method sono entrambe sotto-classi di una classe comune Executable. Ed ereditano da essa molti dei loro metodi. Comune ad entrambi ci sono i parametri e quindi definiamo un metodo per trattarli,

private static String parameterToStr(Parameter p) {
    String s = annMods("", p.getDeclaredAnnotations(), p.getModifiers());
    s += (s.isEmpty() ? "" : " ") + typeToStr(p.getParameterizedType());
    return s + " " + p.getName();
}

Ogni parametro è rappresentato da un oggetto Parameter. Da questo oggetto si possono ottenere le eventuali annotazioni getDeclaredAnnotations(), i modificatori getModifiers(), il tipo getParameterizedType(), che può anche essere parametrico, e infine il nome getName()3. Ora possiamo definire il metodo che permette di trattare i costruttori e i metodi,

public static String executableToStr(Executable exe) {
    String s = exe.isSynthetic() ? "SYNTHETIC": "";
    s = annMods(s, exe.getDeclaredAnnotations(), exe.getModifiers());
    s += typeParametersToStr(" ", exe.getTypeParameters());
    if (exe instanceof Method)
        s += (s.isEmpty() ? "" : " ") + typeToStr(((Method)exe).getGenericReturnType());
    s += (s.isEmpty() ? "" : " ") + simple(exe.getName())+"(";
    Parameter[] pp = exe.getParameters();
    for (int i = 0 ; i < pp.length ; i++)
       s += (i == 0 ? "" : ", ")+parameterToStr(pp[i]);
    return s+")";
}

Prima di tutto controlliamo se il metodo è sintetico. Alcuni metodi di una classe o interfaccia non sono definiti nei sorgenti del programma ma sono introdotti dalla JVM e sono marcati come synthetic. Il metodo isSynthetic() ritorna true se il metodo è appunto sintetico. L'unica differenza tra le intestazioni di metodi e costruttori sta nel tipo del valore ritornato, per questo controlliamo se l'oggetto Executable è un Method e in tal caso con il metodo Type getGenericReturnType() di Method ottiene il tipo del valore ritornato. Il nome del metodo o costruttore ritornato da getName() include anche il package e la classe/interfaccia, per questo lo semplifichiamo con simple. Infine con il metodo getParameters() si ottiene l'array dei parametri.

Campi

Per i campi otteniamo le varie proprietà con metodi che ormai abbiamo già visto all'opera per gli altri membri,

public static String fieldToStr(Field f) {
    String s = f.isSynthetic() ? "SYNTHETIC": "";
    s = annMods(s, f.getDeclaredAnnotations(), f.getModifiers());
    s += (s.isEmpty() ? "" : " ")+typeToStr(f.getGenericType());
    return s + " "+ f.getName();
}

Ora basta modificare il metodo classToString usando i metodi che abbiamo introdotto,

public static String classToString(Class<?> c, String indent) {
    String s = "";
    String ind = indent+"    ";
    try {
        s += indent + classToStr(c)+"\n";      // Intestazione della classe
        for (Class<?> ec : c.getDeclaredClasses())       // Classi annidate
           s += classToString(ec, ind);
        for (Constructor<?> co : c.getDeclaredConstructors()) //Costruttori
            s += ind + executableToStr(co)+" {\n"; // Intestazione costruttore
        s += "\n";
        for (Method m : c.getDeclaredMethods())               // Metodi
            s += ind + executableToStr(m)+"\n";    // Intestazione metodo
        s += "\n";
        for (Field f : c.getDeclaredFields())                 // Campi
            s += ind + fieldToStr(f)+"\n";         // Intestazione campo
        s += indent+"}\n";
    } catch(Exception e) { s += "\nERROR: "+e; }
    return s;
}

Se proviamo la nuova versione ad esempio con

public final Class<T> implements Serializable GenericDeclaration Type AnnotatedElement {
    private static AnnotationData {
        AnnotationData(Map<Class<Annotation> arg0, Map<Class<Annotation> arg1, int arg2)


        final Map<Class<Annotation> annotations
        final Map<Class<Annotation> declaredAnnotations
        final int redefinedCount
    }
    static MethodArray {
        MethodArray()
        MethodArray(int arg0)

        void add(Method arg0)
        private void remove(int arg0)
        Method get(int arg0)
        . . .
        private boolean matchesNameAndDescriptor(Method arg0, Method arg1)
        static boolean hasMoreSpecificClass(Method arg0, Method arg1)

        private Method; methods
        private int length
        private int defaults
    }
    private static ReflectionData<T> {
        ReflectionData(int arg0)

        volatile Constructor<T>[] declaredConstructors
        volatile Constructor<T>[] publicConstructors
        volatile Field; declaredPublicFields
        volatile Method; declaredPublicMethods
        volatile Class<?>[] interfaces
        final int redefinedCount
    }
    private Class(ClassLoader arg0)

    private void checkPackageAccess(ClassLoader arg0, boolean arg1)
    @sun.reflect.CallerSensitive() public static Class<?> forName(String arg0)
    public boolean isSynthetic()
    private native String getName0()
    @sun.reflect.CallerSensitive() public ClassLoader getClassLoader()
    ClassLoader getClassLoader0()
    public TypeVariable<Class<T>>[] getTypeParameters()
    public Type getGenericSuperclass()
    public Package getPackage()
    public Class<?>[] getInterfaces()
    private native Class<?>[] getInterfaces0()
    public Type; getGenericInterfaces()
    `
    private native Object; getEnclosingMethod0()
    public boolean isMemberClass()
    private String getSimpleBinaryName()
    @sun.reflect.CallerSensitive() public Class<?>[] getClasses()
    @sun.reflect.CallerSensitive() public Field; getFields()
    @sun.reflect.CallerSensitive() public Method; getMethods()
    @sun.reflect.CallerSensitive() public Constructor<?>[] getConstructors()
    @sun.reflect.CallerSensitive() public Class<?>[] getDeclaredClasses()
    @sun.reflect.CallerSensitive() public Field; getDeclaredFields()
    @sun.reflect.CallerSensitive() public Method; getDeclaredMethods()
    @sun.reflect.CallerSensitive() public Constructor<?>[] getDeclaredConstructors()
    . . .
    native class [B getRawAnnotations()
    native class [B getRawTypeAnnotations()
    static class [B getExecutableTypeAnnotationBytes(Executable arg0)
    native ConstantPool getConstantPool()
    private static void addAll(Collection<Field> arg0, Field; arg1)
    private Constructor<T>[] privateGetDeclaredConstructors(boolean arg0)
    private Method; privateGetDeclaredMethods(boolean arg0)
    private Method; privateGetPublicMethods()
    private static Field; copyFields(Field; arg0)
    private static Method; copyMethods(Method; arg0)
    private static <U> Constructor<U>[] copyConstructors(Constructor<U>[] arg0)
    private native Field; getDeclaredFields0(boolean arg0)
    public T[] getEnumConstants()
    T[] getEnumConstantsShared()
    public T cast(Object arg0)
    public <A> A[] getAnnotationsByType(Class<A> arg0)
    public Annotation; getAnnotations()
    public <A> A getDeclaredAnnotation(Class<A> arg0)
    public <A> A[] getDeclaredAnnotationsByType(Class<A> arg0)
    public Annotation; getDeclaredAnnotations()
    public AnnotatedType; getAnnotatedInterfaces()
    SYNTHETIC static Field; 100(Class arg0, boolean arg1)
    SYNTHETIC static Field 200(Field; arg0, String arg1)
    SYNTHETIC static boolean 300(Object; arg0, Object; arg1)
    SYNTHETIC static boolean 402(boolean arg0)
    SYNTHETIC static boolean 502(boolean arg0)

    private static final int ANNOTATION
    private transient volatile Class<?> newInstanceCallerCache
    private transient String name
    private static ProtectionDomain allPermDomain
    private static boolean useCaches
    private static final long serialVersionUID
    private static final ObjectStreamField; serialPersistentFields
    private static ReflectionFactory reflectionFactory
    private static boolean initted
    private transient volatile T[] enumConstants
    private transient volatile Map<String, T> enumConstantDirectory
    transient ClassValueMap classValueMap
}

Si può notare che la rappresentazione degli array non è buona. Quelli parametrici sono rappresentati bene ma quelli non parametrici no, ad es. public native Object; getSigners() e public Annotation; getAnnotations() il primo ritorna Object[] e il secondo Annotation[]. La correzione è lasciata come esercizio [Array].

ToString

Una delle possibili applicazioni della reflection è definire metodi che possano trattare in modo uniforme oggetti di tipo diverso. Una di queste applicazioni consiste nel definire un metodo toString(Object x) generale capace di produrre una rappresentazione tramite una stringa di un qualsiasi oggetto x qualunque sia il suo tipo. Nella classe Utils definiamo il seguente metodo,

public static String toString(Object x) {
    if (x == null) return ""+x;
    Class<?> c = x.getClass();
    String s = c.getSimpleName();
    Field[] ff = c.getDeclaredFields();
    if (ff.length > 0) {
        s += "[";
        for (int i = 0 ; i < ff.length ; i++) {
            ff[i].setAccessible(true);
            try {
                s += (i == 0 ? "" : ",")+ff[i].getName()+"="+ff[i].get(x);
            } catch (IllegalAccessException e) { }
        }
        s += "]";
    }
    return s;
}

La rappresentazione è il nome della classe dell'oggetto seguito dalla lista dei nomi dei campi e dei loro valori tra parentesi quadre. C'è solamente una cosa da precisare per poter leggere il valore di un campo, pubblico o privato, tramite il metodo Object get(Object obj) di Field, è necessario preventivamente impostare l'accessibilità con il metodo void setAccessible(boolean flag).

Possiamo provare il metodo con la classe Dipendente,

@Override
public String toString() {
    return mp.reflect.Utils.toString(this);
}

Provandolo con

    Dipendente d = new Dipendente("Mario Rossi");
    out.println(d);

otteniamo

Dipendente[ultimoCodice=1,nomeCognome=Mario Rossi,stipendio=0.0,
           contatti=mp.Dipendente$Contatti@49476842,supervisore=null,codice=1]

Il metodo può essere migliorato. Alcuni valori non sono rappresentati in modo adeguato, ad es. l'oggetto Dipendente.Contatti. In tali casi si potrebbe riapplicare toString(Object x) anche ad esso. È abbastanza chiaro però che in generale non si vogliono mostrare tutti i campi di un oggetto. Per personalizzare il metodo toString(Object x) preservando la sua generalità si potrebbe modificarlo in modo tale che riporti nella rappresentazione solamente i campi annotati con un'apposita annotazione. Ad esempio la seguente annotazione @ToString,

package mp.reflect;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.TYPE,ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ToString {
    boolean name() default true;
}

L'annotazione può essere applicata alla dichiarazione di un tipo ElementType.TYPE o di un campo ElementType.FIELD. Deve essere esplicitamente specificato che l'annotazione sia mantenuta al runtime con RetentionPolicy.RUNTIME. Il parametro name() serve a indicare se il nome della classe o del campo deve essere riportato.

Stimare la dimensione di un oggetto

Un'altra applicazione è la stima della dimensione in byte di un oggetto. La procedura richiede la reflection per poter accedere a tutti campi di un oggetto e alle super-classi della sua classe. La procedura di seguito riportata non fornisce una misurazione esatta ma solamente una buona approssimazione della dimensione effettiva. Introduciamo in mp.reflect una nuova classe ObjSize nella definiamo il metodo statico per fare il calcolo,

package mp.reflect;

/** La classe definisce il metodo {@link ObjSize#estimate(Object)} che ritorna la
 * una stima della dimensione in bytes di un oggetto dato. Partendo dalla classe
 * dell'oggetto conteggia la dimensione dell'oggetto base, dei suoi campi e per i
 * campi riferimento conteggia le dimensioni dei valori facendo attenzione a non
 * conteggiare più volte lo stesso valore (oggetto). Per fare ciò esegue una visita
 * a partire dall'oggetto e seguendo i puntatori contenuti nei campi riferimento. */
public class ObjSize {
    /** Ritorna una stima della dimensione in byte dell'oggetto specificato.
     * @param o  un oggetto
     * @return una stima della dimensione in byte dell'oggetto specificato */
    public static long estimate(Object o) {
        // TODO
    }
}

Procediamo per passi, prima di tutto le basi. Vale a dire il calcolo della dimensione dei tipi primitivi e le dimensioni di un Object e di un campo riferimento,

/** Ritorna la dimensione in byte del tipo primitivo specificato.
* @param c  la classe di un tipo primitivo
* @return la dimensione in byte del tipo primitivo specificato */
private static long primitive(Class<?> c) {
    if (c == byte.class) return 1;
    else if (c == short.class) return 2;
    else if (c == int.class) return 4;
    else if (c == long.class) return 8;
    else if (c == float.class) return 4;
    else if (c == double.class) return 8;
    else if (c == char.class) return 2;
    else if (c == boolean.class) return 1;
    else throw new IllegalArgumentException();
}

/** Ritorna l'aggiustamento della dimensione specificata rispetto alla
* dimensione di allineamento.
* @param s  una dimensione in bytes
* @return l'aggiustamento della dimensione specificata rispetto alla
* dimensione di allineamento */
private static long align(long s) {
    long r = s % ALIGN_SIZE;
    return r == 0 ? s : s + ALIGN_SIZE - r;
}

private static final long REF_SIZE = 8;   // Dimensione variabile riferimento
private static final long ALIGN_SIZE = 8; // Dimensione di allineamento
private static final long OBJ_SIZE = 12;  // Dimensione Object (16)

Il metodo align serve a tener conto che i blocchi nella memoria sono sempre allineati rispetto a blocchi di dimensione minima, a volte chiamati word, e che dipendono dall'architettura della macchina ma generalmente sono determinati dalla dimensione degli indirizzi di memoria che la macchina può gestire. Una machina a 64bit tipicamente ha word da 64 bit, cioè 8 byte.

Ora occupiamoci della dimensione di quello che può essere chiamato il guscio di un oggetto, cioè la dimensione di un oggetto esclusa quella degli eventuali oggetti i cui riferimenti sono mantenuti nei campi. La dimensione del guscio di una classe è la stessa per tutti gli oggetti istanze dalla classe. Per evitare di dover ricalcolare la dimensione di un guscio più volte, quelli già calcolati sono registrati in una mappa. Oltre alla dimensione del guscio ci registriamo anche i campi riferimento perchè poi ci serviranno per conteggiare le dimensioni dei valori di tali campi.

/** Info di una classe. I campi statici sono ignorati */
private static class CInfo {
    /** Dimensione del <i>guscio</i> di una classe, cioè la dimensione di
     * un oggetto della classe esclusa la dimensione dei valori dei campi
     * di tipo riferimento */
    final long size;
    /** Lista di tutti i campi di tipo riferimento */
    final List<Field> refFields;

    CInfo(long size, List<Field> rFF) {
        this.size = size;
        refFields = rFF;
    }
}

/** Ritorna l'info della classe specificata. Se non è stata già calcolata,
 * la calcola e prima di ritornarla la registra.
 * @param c  una classe
 * @return l'info della classe specificata */
private static CInfo getCInfo(Class<?> c) {
    if (!cInfos.containsKey(c)) {   // Se l'info non è già stata calcolata
        CInfo supCI = getCInfo(c.getSuperclass()); // Info della superclasse
        long size = supCI.size;  // La dimensione del guscio comprende quella
                                 // della superclasse e i campi riferimento
                                 // comprendono quelli della superclasse
        List<Field> rFF = new ArrayList<>(supCI.refFields);
        for (Field f : c.getDeclaredFields()) { // Per ogni campo non statico,
            if (Modifier.isStatic(f.getModifiers())) continue;
            Class<?> cf = f.getType();    // ottiene il tipo del campo;
            if (cf.isPrimitive()) {       // se il tipo è primitivo, addiziona
                size += primitive(cf);    // la dimensione a quella del guscio;
            } else {                      // se non è un tipo primitivo,
                f.setAccessible(true);    // permette l'accesso al valore e
                size += REF_SIZE;         // addiziona la dimensione del campo
                rFF.add(f);               // a quella del guscio e aggiunge il
            }                             // campo alla lista dei campi.
        }                                             // Registra l'info
        cInfos.put(c, new CInfo(align(size), rFF));   // della classe
    }
    return cInfos.get(c);
}  

/** Mappa per registrare le informazioni delle classi ed evitare così di
 * ricalcolarle più volte. */
private static final Map<Class<?>, CInfo> cInfos = new HashMap<>();
static {
    cInfos.put(Object.class,new CInfo(OBJ_SIZE, new ArrayList<>()));
}

Il blocco static {} esegue una cosidetta inizializzazione statica e serve appunto a inizializzare campi statici di una classe. Ora possiamo definire il metodo principale,

public static long estimate(Object o) {
    // Per registrare gli oggetti la cui dimensione è stata già conteggiata
    IdentityHashMap<Object,Void> counted = new IdentityHashMap<>();
    class Size {
        long compute(Object o) {  // Ritorna la dimensione dell'oggetto o
            if (o == null || counted.containsKey(o))  // Se è già stato
                return 0;            // conteggiato, non lo riconteggia
            long size = 0;
            counted.put(o,null);     // Lo aggiunge agli oggetti conteggiati
            Class<?> c = o.getClass();   // La classe dell'oggetto
            if (c.isArray()) {           // Se è un array
                // Addiziona la dimensione di Object e del campo length
                size += align(OBJ_SIZE+primitive(int.class));
                int len = Array.getLength(o);        // Ottiene la lunghezza e
                Class<?> cc = c.getComponentType();  // il tipo delle componenti
                if (cc.isPrimitive()) {  // Se il tipo delle componenti è primitivo
                    // Addiziona le dimensioni degli elementi
                    size += align(len*primitive(cc));
                } else {          // Se il tipo delle componenti è riferimento
                    // Addiziona le dimensioni delle componenti riferimento
                    size += align(len*REF_SIZE);
                    // Per ogni elemento calcola (ricorsivamente) la dimensione
                    for (int i = 0 ; i < len ; i++) // e l'addiziona
                        size += new Size().compute(Array.get(o, i));
                }
            } else {                     // Se non è un array
                CInfo ci = getCInfo(c);  // L'info della classe
                size += ci.size;         // Addiziona la dimensione del guscio
                for (Field f : ci.refFields)  // Per ogni campo riferimento
                    try {     // addiziona la dimensione del valore calcolandolo
                        size += new Size().compute(f.get(o));  // ricorsivamente
                    } catch (IllegalAccessException e) {}
            }
            return size;
        }
    }
    return new Size().compute(o);
}

A questo punto può essere provato per stimare la dimensione di qualsiasi oggetto.

Quello che abbiamo visto non esaurisce l'argomento. La reflection può essere usata anche per modificare i valori dei campi di un oggetto e per invocare sia metodi che costruttori.

Esercizi

[Ordine]    Si modifichi il metodo classToString così che per ogni categoria, classe/interfaccia, costruttore, metodo, campo, elenchi i membri di quella categoria in un ordine fissato. Ad esempio static public, public, protected, default, static default, static private, private.

[Array]    Migliorare la rappresentazione degli array in modo tale che quando un tipo è un array sia rappresentato in modo adeguato qualunque sia il tipo delle componenti.

[ToString]    Migliorare il metodo toString(Object x), sfruttando anche l'annotazione definita.

25 Mag 2016


  1. La variabile di tipo T di Constructor<T> è il tipo della classe del costruttore.

  2. Anche le variabili locali ma adesso queste non ci interessano.

  3. Non sempre i nomi dei parametri sono disponibili, se non lo sono il metodo ritorna un nome sintetico del tipo argk, dove k è un indice che numera i parametri a partire da 0.