CodeGym /Java Blog /Random-IT /Esempi di riflessione
John Squirrels
Livello 41
San Francisco

Esempi di riflessione

Pubblicato nel gruppo Random-IT
Forse hai incontrato il concetto di "riflesso" nella vita ordinaria. Questa parola di solito si riferisce al processo di studio di se stessi. Nella programmazione ha un significato simile: è un meccanismo per analizzare i dati su un programma e persino modificare la struttura e il comportamento di un programma mentre il programma è in esecuzione. Esempi di riflessione - 1 L'importante qui è che lo stiamo facendo in fase di esecuzione, non in fase di compilazione. Ma perché esaminare il codice in fase di esecuzione? Dopotutto, puoi già leggere il codice :/ C'è un motivo per cui l'idea di riflessione potrebbe non essere immediatamente chiara: fino a questo punto sapevi sempre con quali classi stavi lavorando. Ad esempio, potresti scrivere una Catclasse:

package learn.codegym;

public class Cat {

   private String name;
   private int age;

   public Cat(String name, int age) {
       this.name = name;
       this.age = age;
   }

   public void sayMeow() {

       System.out.println("Meow!");
   }

   public void jump() {

       System.out.println("Jump!");
   }

   public String getName() {
       return name;
   }

   public void setName(String name) {
       this.name = name;
   }

   public int getAge() {
       return age;
   }

   public void setAge(int age) {
       this.age = age;
   }

@Override
public String toString() {
   return "Cat{" +
           "name='" + name + '\'' +
           ", age=" + age +
           '}';
}

}
Sai tutto su di esso e puoi vedere i campi e i metodi che ha. Supponiamo che tu abbia improvvisamente bisogno di introdurre altre classi di animali nel programma. Probabilmente potresti creare una struttura di ereditarietà di classe con una Animalclasse genitore per comodità. In precedenza, abbiamo persino creato una classe che rappresenta una clinica veterinaria, alla quale potevamo passare un Animaloggetto (istanza di una classe genitore), e il programma trattava l'animale in modo appropriato in base al fatto che fosse un cane o un gatto. Anche se questi non sono i compiti più semplici, il programma è in grado di apprendere tutte le informazioni necessarie sulle classi in fase di compilazione. Di conseguenza, quando passi un Catoggetto ai metodi della classe clinica veterinaria nelmain()metodo, il programma sa già che si tratta di un gatto, non di un cane. Ora immaginiamo di dover affrontare un compito diverso. Il nostro obiettivo è scrivere un analizzatore di codice. Dobbiamo creare una CodeAnalyzerclasse con un solo metodo: void analyzeObject(Object o). Questo metodo dovrebbe:
  • determinare la classe dell'oggetto che gli è passato e visualizzare il nome della classe sulla console;
  • determinare i nomi di tutti i campi della classe passata, inclusi quelli privati, e visualizzarli sulla console;
  • determinare i nomi di tutti i metodi della classe passata, inclusi quelli privati, e visualizzarli sulla console.
Sarà simile a questo:

public class CodeAnalyzer {

   public static void analyzeClass(Object o) {
      
       // Print the name of the class of object o
       // Print the names of all variables of this class
       // Print the names of all methods of this class
   }
  
}
Ora possiamo vedere chiaramente come questo compito differisce da altri compiti che hai risolto in precedenza. Con il nostro obiettivo attuale, la difficoltà sta nel fatto che né noi né il programma sappiamo esattamente cosa verrà passato alanalyzeClass()metodo. Se scrivi un programma del genere, altri programmatori inizieranno a usarlo e potrebbero passare qualsiasi cosa a questo metodo: qualsiasi classe Java standard o qualsiasi altra classe che scrivono. La classe passata può avere qualsiasi numero di variabili e metodi. In altre parole, noi (e il nostro programma) non abbiamo idea con quali classi lavoreremo. Tuttavia, dobbiamo completare questo compito. Ed è qui che l'API Java Reflection standard viene in nostro aiuto. L'API Reflection è un potente strumento del linguaggio. La documentazione ufficiale di Oracle raccomanda che questo meccanismo sia utilizzato solo da programmatori esperti che sanno cosa stanno facendo. Capirai presto perché stiamo dando questo tipo di avviso in anticipo :) Ecco un elenco di cose che puoi fare con l'API Reflection:
  1. Identificare/determinare la classe di un oggetto.
  2. Ottieni informazioni su modificatori di classe, campi, metodi, costanti, costruttori e superclassi.
  3. Scopri quali metodi appartengono a una o più interfacce implementate.
  4. Crea un'istanza di una classe il cui nome di classe non è noto finché il programma non viene eseguito.
  5. Ottenere e impostare il valore di un campo di istanza in base al nome.
  6. Chiama un metodo di istanza per nome.
Elenco impressionante, eh? :) Nota:il meccanismo di riflessione può fare tutte queste cose "al volo", indipendentemente dal tipo di oggetto che passiamo al nostro analizzatore di codice! Esploriamo le funzionalità dell'API Reflection esaminando alcuni esempi.

Come identificare/determinare la classe di un oggetto

Iniziamo con le basi. Il punto di ingresso al motore di riflessione Java è la Classclasse. Sì, sembra davvero divertente, ma questo è il riflesso :) Usando la Classclasse, per prima cosa determiniamo la classe di qualsiasi oggetto passato al nostro metodo. Proviamo a fare questo:

import learn.codegym.Cat;

public class CodeAnalyzer {

   public static void analyzeClass(Object o) {
       Class clazz = o.getClass();
       System.out.println(clazz);
   }

   public static void main(String[] args) {

       analyzeClass(new Cat("Fluffy", 6));
   }
}
Uscita console:

class learn.codegym.Cat
Fai attenzione a due cose. Innanzitutto, abbiamo deliberatamente inserito la Catclasse in un learn.codegympacchetto separato. Ora puoi vedere che il getClass()metodo restituisce il nome completo della classe. In secondo luogo, abbiamo chiamato la nostra variabile clazz. Sembra un po' strano. Avrebbe senso chiamarlo "classe", ma "classe" è una parola riservata in Java. Il compilatore non consentirà alle variabili di essere chiamate così. Dovevamo aggirare il problema in qualche modo :) Non male per cominciare! Cos'altro avevamo in quell'elenco di capacità?

Come ottenere informazioni su modificatori di classe, campi, metodi, costanti, costruttori e superclassi.

Ora le cose si fanno più interessanti! Nella classe corrente, non abbiamo costanti o una classe genitore. Aggiungiamoli per creare un'immagine completa. Crea la Animalclasse genitore più semplice:

package learn.codegym;
public class Animal {

   private String name;
   private int age;
}
E faremo Catereditare la nostra classe Animale aggiungeremo una costante:

package learn.codegym;

public class Cat extends Animal {

   private static final String ANIMAL_FAMILY = "Feline family";

   private String name;
   private int age;

   // ...the rest of the class
}
Ora abbiamo il quadro completo! Vediamo di cosa è capace la riflessione :)

import learn.codegym.Cat;

import java.util.Arrays;

public class CodeAnalyzer {

   public static void analyzeClass(Object o) {
       Class clazz = o.getClass();
       System.out.println("Class name: " + clazz);
       System.out.println("Class fields: " + Arrays.toString(clazz.getDeclaredFields()));
       System.out.println("Parent class: " + clazz.getSuperclass());
       System.out.println("Class methods: " + Arrays.toString(clazz.getDeclaredMethods()));
       System.out.println("Class constructors: " + Arrays.toString(clazz.getConstructors()));
   }

   public static void main(String[] args) {

       analyzeClass(new Cat("Fluffy", 6));
   }
}
Ecco cosa vediamo sulla console:

Class name:  class learn.codegym.Cat 
Class fields: [private static final java.lang.String learn.codegym.Cat.ANIMAL_FAMILY, private java.lang.String learn.codegym.Cat.name, private int learn.codegym.Cat.age] 
Parent class: class learn.codegym.Animal 
Class methods: [public java.lang.String learn.codegym.Cat.getName(), public void learn.codegym.Cat.setName(java.lang.String), public void learn.codegym.Cat.sayMeow(), public void learn.codegym.Cat.setAge(int), public void learn.codegym.Cat.jump(), public int learn.codegym.Cat.getAge()] 
Class constructors: [public learn.codegym.Cat(java.lang.String, int)]
Guarda tutte quelle informazioni dettagliate sulla classe che siamo riusciti a ottenere! E non solo informazioni pubbliche, ma anche informazioni private! Nota: privateanche le variabili vengono visualizzate nell'elenco. La nostra "analisi" della classe si può considerare sostanzialmente completa: stiamo usando il analyzeObject()metodo per imparare tutto quello che possiamo. Ma non è tutto ciò che possiamo fare con la riflessione. Non ci limitiamo alla semplice osservazione: passeremo all'azione! :)

Come creare un'istanza di una classe il cui nome di classe non è noto finché il programma non viene eseguito.

Iniziamo con il costruttore predefinito. La nostra Catclasse non ne ha ancora uno, quindi aggiungiamolo:

public Cat() {
  
}
Ecco il codice per creare un Catoggetto usando createCat()il metodo reflection ():

import learn.codegym.Cat;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class Main {

   public static Cat createCat() throws IOException, IllegalAccessException, InstantiationException, ClassNotFoundException {

       BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
       String className = reader.readLine();

       Class clazz = Class.forName(className);
       Cat cat = (Cat) clazz.newInstance();

       return cat;
   }

public static Object createObject() throws Exception {

   BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
   String className = reader.readLine();

   Class clazz = Class.forName(className);
   Object result = clazz.newInstance();

   return result;
}

   public static void main(String[] args) throws IOException, IllegalAccessException, ClassNotFoundException, InstantiationException {
       System.out.println(createCat());
   }
}
Ingresso console:

learn.codegym.Cat
Uscita console:

Cat{name='null', age=0}
Questo non è un errore: i valori di namee agevengono visualizzati sulla console perché abbiamo scritto il codice per emetterli nel toString()metodo della Catclasse. Qui leggiamo il nome di una classe il cui oggetto creeremo dalla console. Il programma riconosce il nome della classe il cui oggetto deve essere creato. Esempi di riflessione - 3Per brevità, abbiamo omesso il codice corretto per la gestione delle eccezioni, che occuperebbe più spazio dell'esempio stesso. In un programma reale, ovviamente, dovresti gestire situazioni che coinvolgono nomi inseriti in modo errato, ecc. Il costruttore predefinito è piuttosto semplice, quindi come puoi vedere, è facile usarlo per creare un'istanza della classe :) Usando il newInstance()metodo , creiamo un nuovo oggetto di questa classe. È un'altra questione se ilCatcostruttore prende gli argomenti come input. Rimuoviamo il costruttore predefinito della classe e proviamo a eseguire nuovamente il nostro codice.

null
java.lang.InstantiationException: learn.codegym.Cat 
at java.lang.Class.newInstance(Class.java:427)
Qualcosa è andato storto! Abbiamo ricevuto un errore perché abbiamo chiamato un metodo per creare un oggetto utilizzando il costruttore predefinito. Ma ora non abbiamo un tale costruttore. Quindi, quando il newInstance()metodo viene eseguito, il meccanismo di riflessione utilizza il nostro vecchio costruttore con due parametri:

public Cat(String name, int age) {
   this.name = name;
   this.age = age;
}
Ma non abbiamo fatto nulla con i parametri, come se ce ne fossimo completamente dimenticati! Usare la riflessione per passare argomenti al costruttore richiede un po' di "creatività":

import learn.codegym.Cat;

import java.lang.reflect.InvocationTargetException;

public class Main {

   public static Cat createCat()  {

       Class clazz = null;
       Cat cat = null;

       try {
           clazz = Class.forName("learn.codegym.Cat");
           Class[] catClassParams = {String.class, int.class};
           cat = (Cat) clazz.getConstructor(catClassParams).newInstance("Fluffy", 6);
       } catch (ClassNotFoundException e) {
           e.printStackTrace();
       } catch (InstantiationException e) {
           e.printStackTrace();
       } catch (IllegalAccessException e) {
           e.printStackTrace();
       } catch (NoSuchMethodException e) {
           e.printStackTrace();
       } catch (InvocationTargetException e) {
           e.printStackTrace();
       }

       return cat;
   }

   public static void main(String[] args) {
       System.out.println(createCat());
   }
}
Uscita console:

Cat{name='Fluffy', age=6}
Diamo un'occhiata più da vicino a ciò che sta accadendo nel nostro programma. Abbiamo creato un array di Classoggetti.

Class[] catClassParams = {String.class, int.class};
Corrispondono ai parametri del nostro costruttore (che ha solo Stringe intparametri). Li passiamo al clazz.getConstructor()metodo e otteniamo l'accesso al costruttore desiderato. Dopodiché, tutto ciò che dobbiamo fare è chiamare il newInstance()metodo con gli argomenti necessari e non dimenticare di eseguire il cast esplicito dell'oggetto al tipo desiderato: Cat.

cat = (Cat) clazz.getConstructor(catClassParams).newInstance("Fluffy", 6);
Ora il nostro oggetto è stato creato con successo! Uscita console:

Cat{name='Fluffy', age=6}
Andando avanti :)

Come ottenere e impostare il valore di un campo di istanza per nome.

Immagina di utilizzare una classe scritta da un altro programmatore. Inoltre, non hai la possibilità di modificarlo. Ad esempio, una libreria di classi già pronta impacchettata in un JAR. Puoi leggere il codice delle classi, ma non puoi modificarlo. Supponiamo che il programmatore che ha creato una delle classi in questa libreria (lascia che sia la nostra vecchia Catclasse), non riuscendo a dormire a sufficienza la notte prima che il progetto fosse finalizzato, abbia rimosso il getter e il setter per il agecampo. Ora questa classe è arrivata da te. Soddisfa tutte le tue esigenze, poiché hai solo bisogno Catdi oggetti nel tuo programma. Ma ti servono per avere un agecampo! Questo è un problema: non possiamo raggiungere il campo, perché ha ilprivatemodificatore, e il getter e il setter sono stati cancellati dallo sviluppatore privato del sonno che ha creato la classe :/ Bene, la riflessione può aiutarci in questa situazione! Abbiamo accesso al codice della Catclasse, quindi possiamo almeno scoprire quali campi contiene e come si chiamano. Armati di queste informazioni, possiamo risolvere il nostro problema:

import learn.codegym.Cat;

import java.lang.reflect.Field;

public class Main {

   public static Cat createCat()  {

       Class clazz = null;
       Cat cat = null;
       try {
           clazz = Class.forName("learn.codegym.Cat");
           cat = (Cat) clazz.newInstance();

           // We got lucky with the name field, since it has a setter
           cat.setName("Fluffy");

           Field age = clazz.getDeclaredField("age");
          
           age.setAccessible(true);

           age.set(cat, 6);

       } catch (IllegalAccessException e) {
           e.printStackTrace();
       } catch (InstantiationException e) {
           e.printStackTrace();
       } catch (ClassNotFoundException e) {
           e.printStackTrace();
       } catch (NoSuchFieldException e) {
           e.printStackTrace();
       }

       return cat;
   }

   public static void main(String[] args) {
       System.out.println(createCat());
   }
}
Come affermato nei commenti, tutto con il namecampo è semplice, dal momento che gli sviluppatori di classe hanno fornito un palleggiatore. Sai già come creare oggetti dai costruttori predefiniti: abbiamo il newInstance()per questo. Ma dovremo armeggiare con il secondo campo. Scopriamo cosa sta succedendo qui :)

Field age = clazz.getDeclaredField("age");
Qui, utilizzando il nostro Class clazzoggetto , accediamo al agecampo tramite il getDeclaredField()metodo. Ci consente di ottenere il campo dell'età come Field ageoggetto. Ma questo non basta, perché non possiamo semplicemente assegnare valori ai privatecampi. Per fare ciò, dobbiamo rendere accessibile il campo utilizzando il setAccessible()metodo:

age.setAccessible(true);
Una volta fatto questo a un campo, possiamo assegnare un valore:

age.set(cat, 6);
Come puoi vedere, il nostro Field ageoggetto ha una specie di setter inside-out a cui passiamo un valore int e l'oggetto a cui assegnare il campo. Eseguiamo il nostro main()metodo e vediamo:

Cat{name='Fluffy', age=6}
Eccellente! Ce l'abbiamo fatta! :) Vediamo cos'altro possiamo fare...

Come chiamare un metodo di istanza per nome.

Cambiamo leggermente la situazione nell'esempio precedente. Diciamo che lo Catsviluppatore della classe non ha commesso errori con getter e setter. Va tutto bene al riguardo. Ora il problema è diverso: c'è un metodo di cui abbiamo sicuramente bisogno, ma lo sviluppatore lo ha reso privato:

private void sayMeow() {

   System.out.println("Meow!");
}
Ciò significa che se creiamo Catoggetti nel nostro programma, non saremo in grado di chiamare il sayMeow()metodo su di essi. Avremo gatti che non miagolano? È strano :/ Come risolveremmo questo problema? Ancora una volta, l'API Reflection ci aiuta! Conosciamo il nome del metodo di cui abbiamo bisogno. Tutto il resto è un tecnicismo:

import learn.codegym.Cat;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class Main {

   public static void invokeSayMeowMethod()  {

       Class clazz = null;
       Cat cat = null;
       try {

           cat = new Cat("Fluffy", 6);
          
           clazz = Class.forName(Cat.class.getName());
          
           Method sayMeow = clazz.getDeclaredMethod("sayMeow");
          
           sayMeow.setAccessible(true);
          
           sayMeow.invoke(cat);
          
       } catch (ClassNotFoundException e) {
           e.printStackTrace();
       } catch (NoSuchMethodException e) {
           e.printStackTrace();
       } catch (IllegalAccessException e) {
           e.printStackTrace();
       } catch (InvocationTargetException e) {
           e.printStackTrace();
       }
   }

   public static void main(String[] args) {
       invokeSayMeowMethod();
   }
}
Qui facciamo più o meno la stessa cosa che abbiamo fatto quando accediamo a un campo privato. Innanzitutto, otteniamo il metodo di cui abbiamo bisogno. È incapsulato in un Methodoggetto:

Method sayMeow = clazz.getDeclaredMethod("sayMeow");
Il getDeclaredMethod()metodo ci consente di accedere ai metodi privati. Successivamente, rendiamo il metodo chiamabile:

sayMeow.setAccessible(true);
E infine, chiamiamo il metodo sull'oggetto desiderato:

sayMeow.invoke(cat);
Qui, la nostra chiamata al metodo sembra una "callback": siamo abituati a usare un punto per puntare un oggetto al metodo desiderato ( cat.sayMeow()), ma quando lavoriamo con reflection, passiamo al metodo l'oggetto su cui vogliamo chiamare quel metodo. Cosa c'è sulla nostra console?

Meow!
Tutto ha funzionato! :) Ora puoi vedere le vaste possibilità che ci offre il meccanismo di riflessione di Java. In situazioni difficili e inaspettate (come i nostri esempi con una classe di una biblioteca chiusa), può davvero aiutarci molto. Ma, come ogni grande potere, comporta grandi responsabilità. Gli svantaggi della riflessione sono descritti in una sezione speciale sul sito Web di Oracle. Ci sono tre svantaggi principali:
  1. Le prestazioni sono peggiori. I metodi chiamati utilizzando la riflessione hanno prestazioni peggiori rispetto ai metodi chiamati in modo normale.

  2. Ci sono restrizioni di sicurezza. Il meccanismo di riflessione ci consente di modificare il comportamento di un programma in fase di esecuzione. Ma sul posto di lavoro, quando lavori a un progetto reale, potresti dover affrontare limitazioni che non lo consentono.

  3. Rischio di esposizione di informazioni interne. È importante capire che la riflessione è una violazione diretta del principio di incapsulamento: ci consente di accedere a campi privati, metodi, ecc. Non credo di dover menzionare che si dovrebbe ricorrere a una violazione diretta e flagrante dei principi di OOP solo nei casi più estremi, quando non ci sono altri modi per risolvere un problema per motivi indipendenti dalla tua volontà.

Usa la riflessione con saggezza e solo in situazioni in cui non può essere evitata, e non dimenticare i suoi difetti. Con questo, la nostra lezione è giunta al termine. Si è rivelato essere piuttosto lungo, ma hai imparato molto oggi :)
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION