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