Saluti, giovane Padawan. In questo articolo ti parlerò della Forza, un potere che i programmatori Java usano solo in situazioni apparentemente impossibili. Il lato oscuro di Java è l'API Reflection. In Java, la reflection viene implementata utilizzando l'API Java Reflection.
Nella mia gerarchia dei pacchetti , il nome completo di MyClass sarebbe "reflection.MyClass". C'è anche un modo semplice per imparare il nome di una classe (restituire il nome della classe come una stringa):
Cos'è il riflesso Java?
C'è una definizione breve, accurata e popolare su Internet. La riflessione ( dal tardo latino reflexio - tornare indietro ) è un meccanismo per esplorare i dati su un programma mentre è in esecuzione. Reflection consente di esplorare informazioni su campi, metodi e costruttori di classi. Reflection consente di lavorare con tipi che non erano presenti in fase di compilazione, ma che sono diventati disponibili in fase di esecuzione. La riflessione e un modello logicamente coerente per l'emissione di informazioni sugli errori consentono di creare un codice dinamico corretto. In altre parole, capire come funziona la riflessione in Java ti aprirà una serie di incredibili opportunità. Puoi letteralmente destreggiarti tra le classi e i loro componenti. Ecco un elenco di base di ciò che consente la riflessione:- Imparare/determinare la classe di un oggetto;
- Ottieni informazioni su modificatori, campi, metodi, costanti, costruttori e superclassi di una classe;
- Scopri quali metodi appartengono alle interfacce implementate;
- Creare un'istanza di una classe il cui nome di classe è sconosciuto fino al runtime;
- Ottenere e impostare i valori dei campi di un 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 puoi vedere, questa è una classe molto semplice. Il costruttore con i parametri è deliberatamente commentato. Ci torneremo più tardi. Se hai guardato attentamente il contenuto della classe, probabilmente hai notato l'assenza di un getter per il campo del nome . Il campo del nome stesso è contrassegnato con il modificatore di accesso privato : non possiamo accedervi al di fuori della classe stessa, il che significa che non possiamo recuperarne il valore. " Allora qual è il problema ?" tu dici. "Aggiungi un getter o modifica il modificatore di accesso". E avresti ragione, a meno cheMyClass
era in una libreria AAR compilata o in un altro modulo privato senza possibilità di apportare modifiche. In pratica, questo accade sempre. E qualche programmatore sbadato si è semplicemente dimenticato di scrivere un getter . Questo è proprio il momento di ricordare la riflessione! Proviamo ad arrivare al campo del nome privato della MyClass
classe:
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
}
Analizziamo cosa è appena successo. In Java esiste una meravigliosa classe chiamata Class
. Rappresenta classi e interfacce in un'applicazione Java eseguibile. Non tratteremo la relazione tra Class
e ClassLoader
, poiché non è questo l'argomento di questo articolo. Successivamente, per recuperare i campi di questa classe, è necessario chiamare il getFields()
metodo. Questo metodo restituirà tutti i campi accessibili di questa classe. Questo non funziona per noi, perché il nostro campo è private , quindi usiamo il getDeclaredFields()
metodo. Questo metodo restituisce anche un array di campi di classe, ma ora include campi privati e protetti . In questo caso, conosciamo il nome del campo che ci interessa, quindi possiamo usare il getDeclaredField(String)
metodo whereString
è il nome del campo desiderato. Nota: getFields()
e getDeclaredFields()
non restituire i campi di una classe genitore! Grande. Abbiamo un Field
oggetto che fa riferimento al nostro nome . Poiché il campo non era pubblico , dobbiamo concedere l'accesso per lavorarci. Il setAccessible(true)
metodo ci permette di procedere oltre. Ora il campo del nome è sotto il nostro completo controllo! Puoi recuperare il suo valore chiamando il metodo Field
dell'oggetto , dove è un'istanza della nostra classe. Convertiamo il tipo in e assegniamo il valore alla nostra variabile name . Se non riusciamo a trovare un setter per impostare un nuovo valore nel campo del nome, puoi utilizzare il metodo set :get(Object)
Object
MyClass
String
field.set(myClass, (String) "new value");
Congratulazioni! Hai appena imparato le basi della riflessione e sei entrato in un campo privato ! Prestare attenzione al try/catch
blocco e ai tipi di eccezioni gestiti. L'IDE ti dirà che la loro presenza è richiesta da sola, ma puoi chiaramente capire dai loro nomi perché sono qui. Andare avanti! Come avrai notato, la nostra MyClass
classe ha già un metodo per visualizzare informazioni sui dati della classe:
private void printData(){
System.out.println(number + name);
}
Ma anche qui questo programmatore ha lasciato le sue impronte digitali. Il metodo ha un modificatore di accesso privato e dobbiamo scrivere il nostro codice per visualizzare i dati ogni volta. Che casino. Dov'è andato il nostro riflesso? Scrivi 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();
}
}
La procedura qui è più o meno la stessa di quella utilizzata per il recupero di un campo. Accediamo al metodo desiderato per nome e concediamo l'accesso ad esso. E sull'oggetto Method
chiamiamo il invoke(Object, Args)
metodo, dove Object
è anche un'istanza della MyClass
classe. Args
sono gli argomenti del metodo, anche se il nostro non ne ha. Ora usiamo la printData
funzione per visualizzare le informazioni:
public static void main(String[] args) {
MyClass myClass = new MyClass();
int number = myClass.getNumber();
String name = null; //?
printData(myClass); // Output: 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 argomenti e perché il costruttore è commentato? Tutto a suo tempo. È chiaro dalla definizione all'inizio che la reflection consente di creare istanze di una classe in fase di esecuzione (mentre il programma è in esecuzione)! Possiamo creare un oggetto usando il nome completo della classe. Il nome completo della classe è il nome della classe, incluso il percorso del relativo package .
MyClass.class.getName()
Usiamo Java reflection per creare un'istanza della classe:
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
}
Quando si avvia un'applicazione Java, non tutte le classi vengono caricate nella JVM. Se il tuo codice non fa riferimento alla MyClass
classe, allora ClassLoader
, che è responsabile del caricamento delle classi nella JVM, non caricherà mai la classe. Ciò significa che devi forzare ClassLoader
a caricarlo e ottenere una descrizione della classe sotto forma di Class
variabile. Questo è il motivo per cui abbiamo il forName(String)
metodo, dove String
è il nome della classe di cui abbiamo bisogno della descrizione. Dopo aver ottenuto l' Сlass
oggetto, la chiamata al metodo newInstance()
restituirà un Object
oggetto creato utilizzando quella descrizione. Non resta che fornire questo oggetto al nsMyClass
classe. Freddo! È stato difficile, ma comprensibile, spero. Ora possiamo creare un'istanza di una classe letteralmente in una riga! Sfortunatamente, l'approccio descritto funzionerà solo con il costruttore predefinito (senza parametri). Come si chiamano metodi e costruttori con parametri? È ora di decommentare il nostro costruttore. Come previsto, newInstance()
non riesce a trovare il costruttore predefinito e non funziona più. Riscriviamo l'istanza della classe:
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
}
Il getConstructors()
metodo dovrebbe essere chiamato sulla definizione della classe per ottenere costruttori di classi, quindi getParameterTypes()
dovrebbe essere chiamato per ottenere i parametri di un costruttore:
Constructor[] constructors = clazz.getConstructors();
for (Constructor constructor : constructors) {
Class[] paramTypes = constructor.getParameterTypes();
for (Class paramType : paramTypes) {
System.out.print(paramType.getName() + " ");
}
System.out.println();
}
Questo ci dà tutti i costruttori e i loro parametri. Nel mio esempio, mi riferisco a un costruttore specifico con parametri specifici precedentemente noti. E per chiamare questo costruttore, usiamo il newInstance
metodo, a cui passiamo i valori di questi parametri. Sarà lo stesso quando si usa invoke
per chiamare i metodi. Ciò pone la domanda: quando è utile chiamare i costruttori attraverso la riflessione? Come già accennato all'inizio, le moderne tecnologie Java non possono fare a meno dell'API Java Reflection. Ad esempio, Dependency Injection (DI), che combina annotazioni con la riflessione di metodi e costruttori per formare il popolare Darerlibreria per lo sviluppo Android. Dopo aver letto questo articolo, puoi tranquillamente considerarti istruito nei modi dell'API Java Reflection. Non per niente chiamano la riflessione il lato oscuro di Java. Rompe completamente il paradigma OOP. In Java, l'incapsulamento nasconde e limita l'accesso di altri a determinati componenti del programma. Quando usiamo il modificatore privato, intendiamo che quel campo sia accessibile solo dall'interno della classe in cui esiste. E costruiamo la successiva architettura del programma sulla base di questo principio. In questo articolo, abbiamo visto come puoi usare la riflessione per forzare la tua strada ovunque. Il modello di design creativo Singletonne è un buon esempio come soluzione architettonica. L'idea di base è che una classe che implementa questo modello avrà solo un'istanza durante l'esecuzione dell'intero programma. Ciò si ottiene aggiungendo il modificatore di accesso privato al costruttore predefinito. E sarebbe molto brutto se un programmatore usasse la riflessione per creare più istanze di tali classi. A proposito, di recente ho sentito un collega porre una domanda molto interessante: una classe che implementa il modello Singleton può essere ereditata? Potrebbe essere che, in questo caso, anche la riflessione sarebbe impotente? Lascia il tuo feedback sull'articolo e la tua risposta nei commenti qui sotto e poni lì le tue domande!
Altre letture: |
---|
GO TO FULL VERSION