JavaRush /Java Blog /Random-IT /API di riflessione. Riflessione. Il lato oscuro di Java
Oleksandr Klymenko
Livello 13
Харків

API di riflessione. Riflessione. Il lato oscuro di Java

Pubblicato nel gruppo Random-IT
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 è...Reflection API
API di riflessione.  Riflessione.  Il lato oscuro di Java - 1
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.
API di riflessione.  Riflessione.  Il lato oscuro di Java - 2
Ecco un elenco di base di ciò che la riflessione consente:
  • 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.
La riflessione è utilizzata in quasi tutte le moderne tecnologie Java. È difficile immaginare se Java come piattaforma avrebbe potuto raggiungere un'adozione così massiccia senza riflettere. Molto probabilmente non potevo. Hai acquisito familiarità con l'idea teorica generale di riflessione, ora passiamo alla sua applicazione pratica! Non studieremo tutti i metodi della Reflection API, ma solo ciò che effettivamente si riscontra nella pratica. Poiché il meccanismo di riflessione prevede il lavoro con le classi, avremo una classe semplice 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 gettero modifica il modificatore di accesso." E avrai ragione, ma cosa succede se MyClasssi 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 privatecampo namedella 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 Classe 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 privatee 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 Objectsi trova un'istanza della nostra classe MyClass. Lo lanciamo Stringe 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 privatecampo! Prestare attenzione al blocco try/catche 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 MyClassha 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 privatee 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 Methodusiamo 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.
API di riflessione.  Riflessione.  Il lato oscuro di Java - 3
Nella mia gerarchia, packageil nome completo MyClasssarà “ 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 ClassLoaderil 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 Сlassla 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()ObjectMyClassnewInstance()
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à invokeper 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 Singletoneredi? 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!
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION