CodeGym/Java Blog/Random-IT/API Reflection: Reflection. Il lato oscuro di Java
John Squirrels
Livello 41
San Francisco

API Reflection: Reflection. Il lato oscuro di Java

Pubblicato nel gruppo Random-IT
membri
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.

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.
La riflessione è utilizzata in quasi tutte le moderne tecnologie Java. È difficile immaginare che Java, come piattaforma, avrebbe potuto ottenere un'adozione così diffusa senza riflettere. Molto probabilmente, non l'avrebbe fatto. Ora che conosci generalmente la riflessione come concetto teorico, procediamo alla sua applicazione pratica! Non impareremo tutti i metodi dell'API Reflection, ma solo quelli che incontrerai effettivamente nella pratica. Poiché la riflessione implica lavorare con le classi, inizieremo con una semplice classe chiamata 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 cheMyClassera 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 MyClassclasse:
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 Classe 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 Fieldoggetto 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 Fielddell'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)ObjectMyClassString
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/catchblocco 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 MyClassclasse 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 Methodchiamiamo il invoke(Object, Args)metodo, dove Objectè anche un'istanza della MyClassclasse. Argssono gli argomenti del metodo, anche se il nostro non ne ha. Ora usiamo la printDatafunzione 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 .
API Reflection: Reflection.  Il lato oscuro di Java - 2
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):
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 MyClassclasse, allora ClassLoader, che è responsabile del caricamento delle classi nella JVM, non caricherà mai la classe. Ciò significa che devi forzare ClassLoadera caricarlo e ottenere una descrizione della classe sotto forma di Classvariabile. 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' Сlassoggetto, la chiamata al metodo newInstance()restituirà un Objectoggetto creato utilizzando quella descrizione. Non resta che fornire questo oggetto al nsMyClassclasse. 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 newInstancemetodo, a cui passiamo i valori di questi parametri. Sarà lo stesso quando si usa invokeper 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:

Commenti
  • Popolari
  • Nuovi
  • Vecchi
Devi avere effettuato l'accesso per lasciare un commento
Questa pagina non ha ancora commenti