A cosa serve l'API Reflection?

Il meccanismo di riflessione di Java consente a uno sviluppatore di apportare modifiche e ottenere informazioni su classi, interfacce, campi e metodi in fase di esecuzione senza conoscerne i nomi.

L'API Reflection consente inoltre di creare nuovi oggetti, chiamare metodi e ottenere o impostare valori di campo.

Facciamo un elenco di tutto ciò che puoi fare usando la riflessione:

  • Identificare/determinare la classe di un oggetto
  • Ottieni informazioni su modificatori di classe, campi, metodi, costanti, costruttori e superclassi
  • Scopri quali metodi appartengono alle interfacce implementate
  • Crea un'istanza di una classe il cui nome di classe non è noto finché il programma non viene eseguito
  • Ottenere e impostare il valore di un campo di istanza in base al nome
  • Chiama un metodo di istanza per nome

Quasi tutte le moderne tecnologie Java utilizzano la riflessione. È alla base della maggior parte dei framework e delle librerie Java/Java EE di oggi, ad esempio:

  • Framework Spring per la creazione di applicazioni web
  • il quadro di test JUnit

Se non hai mai incontrato questi meccanismi prima, probabilmente ti starai chiedendo perché tutto questo è necessario. La risposta è abbastanza semplice ma anche molto vaga: la riflessione aumenta notevolmente la flessibilità e la capacità di personalizzare la nostra applicazione e il codice.

Ma ci sono sempre pro e contro. Quindi menzioniamo alcuni contro:

  • Violazioni della sicurezza dell'applicazione. La riflessione ci consente di accedere al codice che non dovremmo (violazione dell'incapsulamento).
  • Restrizioni di sicurezza. Reflection richiede autorizzazioni di runtime che non sono disponibili per i sistemi che eseguono un gestore della sicurezza.
  • Prestazioni basse. Reflection in Java determina i tipi in modo dinamico analizzando il classpath per trovare la classe da caricare. Ciò riduce le prestazioni del programma.
  • Difficile da mantenere. Il codice che utilizza la riflessione è difficile da leggere ed eseguire il debug. È meno flessibile e più difficile da mantenere.

Lavorare con le classi utilizzando l'API Reflection

Tutte le operazioni di riflessione iniziano con un oggetto java.lang.Class . Per ogni tipo di oggetto, viene creata un'istanza immutabile di java.lang.Class . Fornisce metodi per ottenere le proprietà degli oggetti, creare nuovi oggetti e chiamare metodi.

Diamo un'occhiata all'elenco dei metodi di base per lavorare con java.lang.Class :

Metodo Azione
Stringa getNome(); Restituisce il nome della classe
int getModifiers(); Restituisce i modificatori di accesso
Pacchetto getPacchetto(); Restituisce informazioni su un pacchetto
Classe getSuperclass(); Restituisce informazioni su una classe padre
Classe[] getInterfaces(); Restituisce un array di interfacce
Costruttore[] getConstructors(); Restituisce informazioni sui costruttori di classi
Campi[] getCampi(); Restituisce i campi di una classe
Campo getField(String fieldName); Restituisce un campo specifico di una classe per nome
Metodo[] getMetodi(); Restituisce un array di metodi

Questi sono i metodi più importanti per ottenere dati su classi, interfacce, campi e metodi. Esistono anche metodi che consentono di ottenere o impostare valori di campo e accedere a campi privati . Li esamineremo un po 'più tardi.

In questo momento parleremo di ottenere la java.lang.Class stessa. Abbiamo tre modi per farlo.

1. Utilizzo di Class.forName

In un'applicazione in esecuzione, è necessario utilizzare il metodo forName(String className) per ottenere una classe.

Questo codice dimostra come possiamo creare classi usando la reflection. Creiamo una classe Person con cui possiamo lavorare:


package com.company;

public class Person {
    private int age;
    private String name;

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

E la seconda parte del nostro esempio è il codice che utilizza la riflessione:


public class TestReflection {
    public static void main(String[] args) {
        try {
            Class<?> aClass = Class.forName("com.company.Person");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

Questo approccio è possibile se si conosce il nome completo della classe. Quindi puoi ottenere la classe corrispondente usando il metodo statico Class.forName() . Questo metodo non può essere utilizzato per i tipi primitivi.

2. Usando .class

Se un tipo è disponibile ma non è presente alcuna istanza, è possibile ottenere la classe aggiungendo .class al nome del tipo. Questo è il modo più semplice per ottenere la classe di un tipo primitivo.


Class aClass = Person.class;

3. Utilizzo di .getClass()

Se un oggetto è disponibile, il modo più semplice per ottenere una classe è chiamare object.getClass() .


Person person = new Person();
Class aClass = person.getClass();

Qual è la differenza tra gli ultimi due approcci?

Usa A.class se sai a quale oggetto di classe sei interessato al momento della codifica. Se non è disponibile alcuna istanza, dovresti utilizzare .class .

Ottenere i metodi di una classe

Diamo un'occhiata ai metodi che restituiscono i metodi della nostra classe: getDeclaredMethods() e getMethods() .

getDeclaredMethods() restituisce un array che contiene oggetti Method per tutti i metodi dichiarati della classe o dell'interfaccia rappresentata dall'oggetto Class, inclusi i metodi public, private, default e protected, ma non i metodi ereditati.

getMethods() restituisce un array contenente oggetti Method per tutti i metodi pubblici della classe o dell'interfaccia rappresentata dall'oggetto Class — quelli dichiarati dalla classe o dall'interfaccia, nonché quelli ereditati dalle superclassi e dalle superinterfacce.

Diamo un'occhiata a come funziona ciascuno di essi.

Iniziamo con getDeclaredMethods() . Per aiutarci ancora una volta a capire la differenza tra i due metodi, di seguito lavoreremo con la classe astratta Numbers . Scriviamo un metodo statico che convertirà il nostro array Method in List<String> :


import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class TestReflection {
    public static void main(String[] args) {
        final Method[] declaredMethods = Number.class.getDeclaredMethods();
        List<String> actualMethodNames = getMethodNames(declaredMethods);
        actualMethodNames.forEach(System.out::println);
    }

    private static List<String> getMethodNames(Method[] methods) {
        return Arrays.stream(methods)
                .map(Method::getName)
                .collect(Collectors.toList());
    }
}

Ecco il risultato dell'esecuzione di questo codice:

byteValue
shortValue
intValue
longValue
float floatValue;
doubleValue

Questi sono i metodi dichiarati all'interno della classe Number . Cosa restituisce getMethods() ? Cambiamo due righe nell'esempio:


final Method[] methods = Number.class.getMethods();
List<String> actualMethodNames = getMethodNames(methods);

In questo modo, vedremo il seguente insieme di metodi:

byteValue
shortValue
intValue
longValue
float floatValue;
doubleValue
wait
wait
wait
uguale
aString
hashCode
getClass
notify
notifyAll

Poiché tutte le classi ereditano Object , il nostro metodo restituisce anche i metodi pubblici della classe Object .

Ottenere i campi di una classe

I metodi getFields e getDeclaredFields vengono utilizzati per ottenere i campi di una classe. Ad esempio, diamo un'occhiata alla classe LocalDateTime . Riscriveremo il nostro codice:


import java.lang.reflect.Field;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class TestReflection {
    public static void main(String[] args) {
        final Field[] declaredFields = LocalDateTime.class.getDeclaredFields();
        List<String> actualFieldNames = getFieldNames(declaredFields);
        actualFieldNames.forEach(System.out::println);
    }

    private static List<String> getFieldNames(Field[] fields) {
        return Arrays.stream(fields)
                .map(Field::getName)
                .collect(Collectors.toList());
    }
}

Come risultato dell'esecuzione di questo codice, otteniamo l'insieme di campi contenuti nella classe LocalDateTime.

MIN
MAX
serialVersionUID
data
ora

Per analogia con il nostro precedente esame dei metodi, vediamo cosa succede se cambiamo un po' il codice:


final Field[] fields = LocalDateTime.class.getFields();
List<String> actualFieldNames = getFieldNames(fields);

Produzione:

MINIMO
MASSIMO

Ora scopriamo la differenza tra questi metodi.

Il metodo getDeclaredFields restituisce un array di oggetti Field per tutti i campi dichiarati dalla classe o dall'interfaccia rappresentata da thisClasseoggetto.

Il metodo getFields restituisce un array di oggetti Field per tutti i campi pubblici della classe o dell'interfaccia rappresentata daClasseoggetto.

Ora diamo un'occhiata all'interno di LocalDateTime .

Quella della classeMINEMASSIMOi campi sono pubblici, il che significa che saranno visibili attraverso il metodo getFields . Al contrario, ildata,tempo,serialVersionUIDi metodi hanno il modificatore privato , il che significa che non saranno visibili attraverso il metodo getFields , ma possiamo ottenerli usando getDeclaredFields . Questo è il modo in cui possiamo accedere agli oggetti Field per i campi privati .

Descrizioni di altri metodi

Ora è il momento di parlare di alcuni metodi della classe Class , vale a dire:

Metodo Azione
getModifiers Ottenere i modificatori per la nostra classe
getPackage Ottenere il pacchetto che contiene la nostra classe
getSuperclass Ottenere la classe genitore
getInterfaces Ottenere una matrice di interfacce implementate da una classe
getNome Ottenere il nome completo della classe
getSimpleName Ottenere il nome di una classe

getModifiers()

È possibile accedere ai modificatori utilizzando aClasseoggetto.

I modificatori sono parole chiave come public , static , interface , ecc. Otteniamo i modificatori usando il metodo getModifiers() :


Class<Person> personClass = Person.class;
int classModifiers = personClass.getModifiers();

Questo codice imposta il valore di anintvariabile che è un campo di bit. Ciascun modificatore di accesso può essere attivato o disattivato impostando o azzerando il bit corrispondente. Possiamo controllare i modificatori usando i metodi nella classe java.lang.reflect.Modifier :


import com.company.Person;
import java.lang.reflect.Modifier;

public class TestReflection {
    public static void main(String[] args) {
        Class<Person> personClass = Person.class;
        int classModifiers = personClass.getModifiers();

        boolean isPublic = Modifier.isPublic(classModifiers);
        boolean isStatic = Modifier.isStatic(classModifiers);
        boolean isFinal = Modifier.isFinal(classModifiers);
        boolean isAbstract = Modifier.isAbstract(classModifiers);
        boolean isInterface = Modifier.isInterface(classModifiers);

        System.out.printf("Class modifiers: %d%n", classModifiers);
        System.out.printf("Is public: %b%n", isPublic);
        System.out.printf("Is static: %b%n", isStatic);
        System.out.printf("Is final: %b%n", isFinal);
        System.out.printf("Is abstract: %b%n", isAbstract);
        System.out.printf("Is interface: %b%n", isInterface);
    }
}

Ricorda come appare la dichiarazione della nostra Persona :


public class Person {
   …
}

Otteniamo il seguente output:

Modificatori di classe: 1
È pubblico: vero
È statico: falso
È finale: falso
È astratto: falso
È interfaccia: falso

Se rendiamo la nostra classe astratta, allora abbiamo:


public abstract class Person { … }

e questo output:

Modificatori di classe: 1025
È pubblico: vero
È statico: falso
È finale: falso
È astratto: vero
È interfaccia: falso

Abbiamo cambiato il modificatore di accesso, il che significa che abbiamo cambiato anche i dati restituiti attraverso i metodi statici della classe Modifier .

getPacchetto()

Conoscendo solo una classe, possiamo ottenere informazioni sul suo pacchetto:


Class<Person> personClass = Person.class;
final Package aPackage = personClass.getPackage();
System.out.println(aPackage.getName());

getSuperclass()

Se abbiamo un oggetto Class, allora possiamo accedere alla sua classe genitore:


public static void main(String[] args) {
    Class<Person> personClass = Person.class;
    final Class<? super Person> superclass = personClass.getSuperclass();
    System.out.println(superclass);
}

Otteniamo la ben nota classe Object :


class java.lang.Object

Ma se la nostra classe ha un'altra classe genitore, la vedremo invece:


package com.company;

class Human {
    // Some info
}

public class Person extends Human {
    private int age;
    private String name;

    // Some info
}

Qui otteniamo la nostra classe genitore:


class com.company.Human

getInterfaces()

Ecco come possiamo ottenere l'elenco delle interfacce implementate dalla classe:


public static void main(String[] args) {
    Class<Person> personClass = Person.class;
    final Class<?>[] interfaces = personClass.getInterfaces();
    System.out.println(Arrays.toString(interfaces));
}

E non dimentichiamo di cambiare la nostra classe Person :


public class Person implements Serializable { … }

Produzione:

[interfaccia java.io.Serializable]

Una classe può implementare molte interfacce. Ecco perché otteniamo una matrice diClasseoggetti. Nell'API Java Reflection, le interfacce sono rappresentate anche daClasseoggetti.

Nota: il metodo restituisce solo le interfacce implementate dalla classe specificata, non la sua classe genitore. Per ottenere un elenco completo delle interfacce implementate dalla classe, è necessario fare riferimento sia alla classe corrente che a tutti i suoi predecessori nella catena di ereditarietà.

getName() & getSimpleName() & getCanonicalName()

Scriviamo un esempio che coinvolge una primitiva, una classe nidificata, una classe anonima e la classe String :


public class TestReflection {
    public static void main(String[] args) {
        printNamesForClass(int.class, "int class (primitive)");
        printNamesForClass(String.class, "String.class (ordinary class)");
        printNamesForClass(java.util.HashMap.SimpleEntry.class,
                "java.util.HashMap.SimpleEntry.class (nested class)");
        printNamesForClass(new java.io.Serializable() {
                }.getClass(),
                "new java.io.Serializable(){}.getClass() (anonymous inner class)");
    }

    private static void printNamesForClass(final Class<?> clazz, final String label) {
        System.out.printf("%s:%n", label);
        System.out.printf("\tgetName()):\t%s%n", clazz.getName());
        System.out.printf("\tgetCanonicalName()):\t%s%n", clazz.getCanonicalName());
        System.out.printf("\tgetSimpleName()):\t%s%n", clazz.getSimpleName());
        System.out.printf("\tgetTypeName():\t%s%n%n", clazz.getTypeName());
    }
}

Risultato del nostro programma:

int class (primitivo):
getName()): int
getCanonicalName()): int
getSimpleName()): int
getTypeName(): int

String.class (classe ordinaria):
getName()): java.lang.String
getCanonicalName() ): java.lang.String
getSimpleName()): String
getTypeName(): java.lang.String

java.util.HashMap.SimpleEntry.class (classe annidata):
getName()): java.util.AbstractMap$SimpleEntry
getCanonicalName( )): java.util.AbstractMap.SimpleEntry
getSimpleName()): SimpleEntry
getTypeName(): java.util.AbstractMap$SimpleEntry

new java.io.Serializable(){}.getClass() (classe interna anonima):
getName() ): TestReflection$1
getCanonicalName()): null
getSimpleName()):
getTypeName(): TestReflection$1

Ora analizziamo l'output del nostro programma:

  • getName() restituisce il nome dell'entità.

  • getCanonicalName() restituisce il nome canonico della classe base, come definito dalla specifica del linguaggio Java. Restituisce null se la classe base non ha un nome canonico (ovvero se è una classe locale o anonima o un array il cui tipo di elemento non ha un nome canonico).

  • getSimpleName() restituisce il nome semplice della classe base come specificato nel codice sorgente. Restituisce una stringa vuota se la classe base è anonima.

  • getTypeName() restituisce una stringa informativa per il nome di questo tipo.