Saluti, giovane Padawan. In questo articolo ti parlerò della Forza, il cui potere i programmatori Java usano solo in una situazione apparentemente senza speranza. Quindi, il lato oscuro di Java è...
La riflessione in Java viene eseguita utilizzando l'API Java Reflection. Cos'è questa riflessione? Esiste una definizione breve e precisa che è popolare anche su Internet. La riflessione (dal tardo latino reflexio - tornare indietro) è un meccanismo per studiare i dati di un programma durante la sua esecuzione. La riflessione consente di esaminare le informazioni su campi, metodi e costruttori di classi. Il meccanismo di riflessione stesso consente di elaborare tipi assenti durante la compilazione, ma apparsi durante l'esecuzione del programma. La riflessione e la presenza di un modello logicamente coerente per la segnalazione degli errori consente di creare un codice dinamico corretto. In altre parole, comprendere come funziona la riflessione in Java ti apre una serie di straordinarie opportunità. Puoi letteralmente destreggiarti tra le classi e i loro componenti.
Ecco un elenco di base di ciò che la riflessione consente:
Nella mia gerarchia,
Reflection API
- Scoprire/determinare la classe di un oggetto;
- Ottieni informazioni su modificatori di classe, campi, metodi, costanti, costruttori e superclassi;
- Scoprire quali metodi appartengono all'interfaccia/interfacce implementate;
- Crea un'istanza di una classe e il nome della classe sarà sconosciuto finché il programma non verrà eseguito;
- Ottieni e imposta il valore di un campo oggetto per nome;
- Chiama il metodo di un oggetto per nome.
MyClass
:
public class MyClass {
private int number;
private String name = "default";
// public MyClass(int number, String name) {
// this.number = number;
// this.name = name;
// }
public int getNumber() {
return number;
}
public void setNumber(int number) {
this.number = number;
}
public void setName(String name) {
this.name = name;
}
private void printData(){
System.out.println(number + name);
}
}
Come possiamo vedere, questa è la classe più comune. Il costruttore con parametri è commentato per un motivo, torneremo su questo più tardi. Se hai osservato attentamente il contenuto della classe, probabilmente hai notato l'assenza di getter
"a" per il file name
. Il campo stesso name
è contrassegnato con un modificatore di accesso private
; non saremo in grado di accedervi al di fuori della classe stessa; =>
non possiamo ottenere il suo valore. "Allora, qual'è il problema? - tu dici. "Aggiungi getter
o modifica il modificatore di accesso." E avrai ragione, ma cosa succede se MyClass
si trova in una libreria aar compilata o in un altro modulo chiuso senza accesso in modifica, e in pratica ciò accade molto spesso. E qualche programmatore disattento si è semplicemente dimenticato di scrivere getter
. È tempo di ricordarsi della riflessione! Proviamo ad arrivare al private
campo name
della classe MyClass
:
public static void main(String[] args) {
MyClass myClass = new MyClass();
int number = myClass.getNumber();
String name = null; //no getter =(
System.out.println(number + name);//output 0null
try {
Field field = myClass.getClass().getDeclaredField("name");
field.setAccessible(true);
name = (String) field.get(myClass);
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
System.out.println(number + name);//output 0default
}
Scopriamo cosa è successo qui adesso. C'è una classe meravigliosa in Java Class
. Rappresenta classi e interfacce in un'applicazione Java eseguibile. Non toccheremo la connessione tra Class
e ClassLoader
. questo non è l'argomento dell'articolo. Successivamente, per ottenere i campi di questa classe, è necessario chiamare il metodo getFields()
, questo metodo ci restituirà tutti i campi disponibili della classe. Questo non è adatto a noi, dato che il nostro campo è private
, quindi utilizziamo il metodo getDeclaredFields()
.. Anche questo metodo restituisce un array di campi di classe, ma ora sia private
e protected
. Nella nostra situazione, conosciamo il nome del campo che ci interessa e possiamo utilizzare il metodo getDeclaredField(String)
, dove String
è il nome del campo desiderato. Nota: getFields()
e getDeclaredFields()
non restituire i campi della classe genitore! Ottimo, abbiamo ricevuto un oggetto Field
con un collegamento al nostro file name
. Perché il campo non era публичным
(pubblico), dovrebbe essere consentito l'accesso per lavorarci. Il metodo setAccessible(true)
ci permette di continuare a lavorare. Ora il campo name
è completamente sotto il nostro controllo! Puoi ottenere il suo valore chiamando get(Object)
l'oggetto Field
, dove Object
si trova un'istanza della nostra classe MyClass
. Lo lanciamo String
e lo assegniamo alla nostra variabile name
. Nel caso in cui all'improvviso non abbiamo più setter
'a, possiamo usare il metodo per impostare un nuovo valore per il campo del nome set
:
field.set(myClass, (String) "new value");
Congratulazioni! Hai appena padroneggiato il meccanismo base della riflessione e sei riuscito ad accedere al private
campo! Prestare attenzione al blocco try/catch
e ai tipi di eccezioni gestiti. L'IDE stesso indicherà la loro presenza obbligatoria, ma il loro nome chiarisce perché sono qui. Andare avanti! Come avrai notato, il nostro MyClass
ha già un metodo per visualizzare le informazioni sui dati della classe:
private void printData(){
System.out.println(number + name);
}
Ma anche qui questo programmatore ha lasciato un'eredità. Il metodo è sotto il modificatore di accesso private
e ogni volta dovevamo scrivere noi stessi il codice di output. Non è in ordine, dov’è la nostra riflessione?... Scriviamo la seguente funzione:
public static void printData(Object myClass){
try {
Method method = myClass.getClass().getDeclaredMethod("printData");
method.setAccessible(true);
method.invoke(myClass);
} catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
e.printStackTrace();
}
}
Qui la procedura è più o meno la stessa di quella per ottenere un campo: otteniamo il metodo desiderato per nome e gli diamo accesso. E per chiamare l'oggetto Method
usiamo invoke(Оbject, Args)
, dove Оbject
è anche un'istanza della classe MyClass
. Args
- argomenti del metodo: il nostro non ne ha. Ora utilizziamo la funzione per visualizzare le informazioni printData
:
public static void main(String[] args) {
MyClass myClass = new MyClass();
int number = myClass.getNumber();
String name = null; //?
printData(myClass); // outout 0default
try {
Field field = myClass.getClass().getDeclaredField("name");
field.setAccessible(true);
field.set(myClass, (String) "new value");
name = (String) field.get(myClass);
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
printData(myClass);// output 0new value
}
Evviva, ora abbiamo accesso al metodo privato della classe. Ma cosa succede se il metodo ha ancora argomenti e perché esiste un costruttore commentato? Tutto ha il suo tempo. Dalla definizione iniziale è chiaro che la riflessione permette di creare istanze di una classe in una modalità runtime
(mentre il programma è in esecuzione)! Possiamo creare un oggetto di una classe con il nome completo di quella classe. Il nome completo della classe è il nome della classe, dato il percorso in package
.
package
il nome completo MyClass
sarà “ reflection.MyClass
”. Puoi anche scoprire il nome della classe in modo semplice (restituirà il nome della classe come una stringa):
MyClass.class.getName()
Creiamo un'istanza della classe utilizzando la riflessione:
public static void main(String[] args) {
MyClass myClass = null;
try {
Class clazz = Class.forName(MyClass.class.getName());
myClass = (MyClass) clazz.newInstance();
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
e.printStackTrace();
}
System.out.println(myClass);//output created object reflection.MyClass@60e53b93
}
Al momento dell'avvio dell'applicazione Java, non tutte le classi vengono caricate nella JVM. Se il tuo codice non fa riferimento alla classe MyClass
, la persona responsabile del caricamento delle classi nella JVM, e cioè ClassLoader
, non lo caricherà mai lì. Dobbiamo quindi forzarne ClassLoader
il caricamento e ricevere una descrizione della nostra classe sotto forma di una variabile di tipo Class
. Per questo compito esiste un metodo forName(String)
, dove String
è il nome della classe di cui richiediamo la descrizione. Dopo aver ricevuto , verrà restituita Сlass
la chiamata al metodo , che verrà creata secondo la stessa descrizione. Resta da portare questo oggetto nella nostra classe . Freddo! È stato difficile, ma spero che sia comprensibile. Ora possiamo creare un'istanza di una classe letteralmente da una riga! Sfortunatamente, il metodo descritto funzionerà solo con il costruttore predefinito (senza parametri). Come chiamare metodi con argomenti e costruttori con parametri? È ora di rimuovere il commento dal nostro costruttore. Come previsto, non trova il costruttore predefinito e non funziona più. Riscriviamo la creazione di un'istanza di classe: newInstance()
Object
MyClass
newInstance()
public static void main(String[] args) {
MyClass myClass = null;
try {
Class clazz = Class.forName(MyClass.class.getName());
Class[] params = {int.class, String.class};
myClass = (MyClass) clazz.getConstructor(params).newInstance(1, "default2");
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
e.printStackTrace();
}
System.out.println(myClass);//output created object reflection.MyClass@60e53b93
}
Per ottenere i costruttori di classi, chiamare il metodo dalla descrizione della classe getConstructors()
e per ottenere i parametri del costruttore, chiamare getParameterTypes()
:
Constructor[] constructors = clazz.getConstructors();
for (Constructor constructor : constructors) {
Class[] paramTypes = constructor.getParameterTypes();
for (Class paramType : paramTypes) {
System.out.print(paramType.getName() + " ");
}
System.out.println();
}
In questo modo otteniamo tutti i costruttori e tutti i parametri. Nel mio esempio, c'è una chiamata a un costruttore specifico con parametri specifici già noti. E per chiamare questo costruttore, utilizziamo il metodo newInstance
, in cui specifichiamo i valori per questi parametri. Lo stesso accadrà invoke
per i metodi di chiamata. Sorge la domanda: dove può essere utile la vocazione riflessiva dei costruttori? Le moderne tecnologie Java, come accennato all'inizio, non possono fare a meno della Reflection API. Ad esempio, DI (Dependency Injection), dove le annotazioni combinate con la riflessione di metodi e costruttori formano la libreria Dagger, popolare nello sviluppo Android. Dopo aver letto questo articolo, puoi tranquillamente considerarti illuminato sui meccanismi dell'API Reflection. Non per niente la riflessione è chiamata il lato oscuro di Java. Rompe completamente il paradigma OOP. In Java, l'incapsulamento serve a nascondere e limitare l'accesso di alcuni componenti del programma ad altri. Usando il modificatore private intendiamo che l'accesso a questo campo sarà solo all'interno della classe in cui esiste questo campo, sulla base di questo costruiamo l'ulteriore architettura del programma. In questo articolo abbiamo visto come utilizzare la riflessione per arrivare ovunque. Un buon esempio sotto forma di soluzione architettonica è il modello di progettazione generativa - Singleton
. La sua idea principale è che durante l'intera operazione del programma, la classe che implementa questo modello dovrebbe avere una sola copia. Questo viene fatto impostando il modificatore di accesso predefinito su private per il costruttore. E sarebbe molto brutto se qualche programmatore con la propria riflessione creasse tali classi. A proposito, c'è una domanda molto interessante che ho sentito di recente dal mio dipendente: una classe che implementa un modello può avere Singleton
eredi? Possibile che anche la riflessione sia impotente in questo caso? Scrivi il tuo feedback sull'articolo e la risposta nei commenti, e fai anche le tue domande! Il vero potere dell'API Reflection è in combinazione con le annotazioni runtime, di cui probabilmente parleremo in un futuro articolo sul lato oscuro di Java. Grazie per l'attenzione!
GO TO FULL VERSION