
Cat
classe:
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 Animal
classe genitore per comodità. In precedenza, abbiamo persino creato una classe che rappresenta una clinica veterinaria, alla quale potevamo passare un Animal
oggetto (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 Cat
oggetto 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 CodeAnalyzer
classe 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.
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:
- Identificare/determinare la classe di un oggetto.
- Ottieni informazioni su modificatori di classe, campi, metodi, costanti, costruttori e superclassi.
- Scopri quali metodi appartengono a una o più interfacce implementate.
- Crea un'istanza di una classe il cui nome di classe non è noto finché il programma non viene eseguito.
- Ottenere e impostare il valore di un campo di istanza in base al nome.
- Chiama un metodo di istanza per nome.
Come identificare/determinare la classe di un oggetto
Iniziamo con le basi. Il punto di ingresso al motore di riflessione Java è laClass
classe. Sì, sembra davvero divertente, ma questo è il riflesso :) Usando la Class
classe, 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 Cat
classe in un learn.codegym
pacchetto 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 laAnimal
classe genitore più semplice:
package learn.codegym;
public class Animal {
private String name;
private int age;
}
E faremo Cat
ereditare la nostra classe Animal
e 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: private
anche 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 nostraCat
classe non ne ha ancora uno, quindi aggiungiamolo:
public Cat() {
}
Ecco il codice per creare un Cat
oggetto 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 name
e age
vengono visualizzati sulla console perché abbiamo scritto il codice per emetterli nel toString()
metodo della Cat
classe. 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. 
newInstance()
metodo , creiamo un nuovo oggetto di questa classe. È un'altra questione se ilCat
costruttore 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 Class
oggetti.
Class[] catClassParams = {String.class, int.class};
Corrispondono ai parametri del nostro costruttore (che ha solo String
e int
parametri). 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 vecchiaCat
classe), non riuscendo a dormire a sufficienza la notte prima che il progetto fosse finalizzato, abbia rimosso il getter e il setter per il age
campo. Ora questa classe è arrivata da te. Soddisfa tutte le tue esigenze, poiché hai solo bisogno Cat
di oggetti nel tuo programma. Ma ti servono per avere un age
campo! Questo è un problema: non possiamo raggiungere il campo, perché ha ilprivate
modificatore, 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 Cat
classe, 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 name
campo è 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 clazz
oggetto , accediamo al age
campo tramite il getDeclaredField()
metodo. Ci consente di ottenere il campo dell'età come Field age
oggetto. Ma questo non basta, perché non possiamo semplicemente assegnare valori ai private
campi. 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 age
oggetto 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 loCat
sviluppatore 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 Cat
oggetti 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 Method
oggetto:
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:
-
Le prestazioni sono peggiori. I metodi chiamati utilizzando la riflessione hanno prestazioni peggiori rispetto ai metodi chiamati in modo normale.
-
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.
-
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à.
GO TO FULL VERSION