Cat
clasă:
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 +
'}';
}
}
Știi totul despre el și poți vedea domeniile și metodele pe care le are. Să presupunem că brusc trebuie să introduceți alte clase de animale în program. Probabil ați putea crea o structură de moștenire a clasei cu o Animal
clasă părinte pentru comoditate. Mai devreme, chiar am creat o clasă reprezentând o clinică veterinară, la care puteam trece un Animal
obiect (instanța unei clase părinte), iar programul a tratat animalul în mod corespunzător în funcție de faptul că era un câine sau o pisică. Chiar dacă acestea nu sunt cele mai simple sarcini, programul este capabil să învețe toate informațiile necesare despre clase în timpul compilării. În consecință, atunci când treceți un Cat
obiect la metodele clasei clinicii veterinare dinmain()
metoda, programul știe deja că este o pisică, nu un câine. Acum să ne imaginăm că ne confruntăm cu o altă sarcină. Scopul nostru este să scriem un analizor de cod. Trebuie să creăm o CodeAnalyzer
clasă cu o singură metodă: void analyzeObject(Object o)
. Această metodă ar trebui să:
- determinați clasa obiectului transmis acestuia și afișați numele clasei pe consolă;
- determinați numele tuturor câmpurilor clasei trecute, inclusiv cele private, și afișați-le pe consolă;
- determinați numele tuturor metodelor din clasa trecută, inclusiv cele private, și afișați-le pe consolă.
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
}
}
Acum putem vedea clar cum diferă această sarcină de alte sarcini pe care le-ați rezolvat anterior. Cu obiectivul nostru actual, dificultatea constă în faptul că nici noi, nici programul nu știm ce anume va fi trecut cătreanalyzeClass()
metodă. Dacă scrieți un astfel de program, alți programatori vor începe să-l folosească și ar putea transmite orice acestei metode - orice clasă Java standard sau orice altă clasă pe care o scriu. Clasa trecută poate avea orice număr de variabile și metode. Cu alte cuvinte, noi (și programul nostru) habar n-avem cu ce clase vom lucra. Dar totuși, trebuie să îndeplinim această sarcină. Și aici ne vine în ajutor API-ul standard Java Reflection. API-ul Reflection este un instrument puternic al limbajului. Documentația oficială Oracle recomandă ca acest mecanism să fie folosit doar de programatori experimentați care știu ce fac. Veți înțelege în curând de ce oferim acest tip de avertisment în avans :) Iată o listă de lucruri pe care le puteți face cu API-ul Reflection:
- Identificarea/determinarea clasei unui obiect.
- Obțineți informații despre modificatorii de clasă, câmpuri, metode, constante, constructori și superclase.
- Aflați ce metode aparțin unei interfețe implementate.
- Creați o instanță a unei clase al cărei nume de clasă nu este cunoscut până când programul este executat.
- Obțineți și setați valoarea unui câmp de instanță după nume.
- Apelați o metodă de instanță după nume.
Cum se identifică/determină clasa unui obiect
Să începem cu elementele de bază. Punctul de intrare în motorul de reflecție Java este clasaClass
. Da, pare foarte amuzant, dar asta este reflecția :) Folosind clasa Class
, determinăm mai întâi clasa oricărui obiect trecut la metoda noastră. Să încercăm să facem asta:
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));
}
}
Ieșire din consolă:
class learn.codegym.Cat
Fii atent la două lucruri. În primul rând, punem în mod deliberat Cat
clasa într-un pachet separat learn.codegym
. Acum puteți vedea că getClass()
metoda returnează numele complet al clasei. În al doilea rând, am numit variabila noastră clazz
. Asta pare un pic ciudat. Ar avea sens să-l numim „clasă”, dar „clasă” este un cuvânt rezervat în Java. Compilatorul nu va permite ca variabilele să fie numite așa. Trebuia să ocolim asta cumva :) Nu e rău pentru început! Ce altceva mai aveam pe acea listă de capabilități?
Cum să obțineți informații despre modificatorii de clasă, câmpuri, metode, constante, constructori și superclase.
Acum lucrurile devin mai interesante! În clasa curentă, nu avem nicio constantă sau o clasă părinte. Să le adăugăm pentru a crea o imagine completă. Creați cea mai simplăAnimal
clasă părinte:
package learn.codegym;
public class Animal {
private String name;
private int age;
}
Și vom face ca Cat
clasa noastră să moștenească Animal
și să adăugăm o constantă:
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
}
Acum avem imaginea completă! Să vedem de ce este capabilă reflecția :)
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));
}
}
Iată ce vedem pe consolă:
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)]
Uită-te la toate informațiile detaliate despre clasă pe care le-am putut obține! Și nu doar informații publice, ci și informații private! Notă: private
variabilele sunt de asemenea afișate în listă. „Analiza noastră” a clasei poate fi considerată în esență completă: folosim metoda analyzeObject()
pentru a învăța tot ce putem. Dar asta nu este tot ce putem face cu reflecția. Nu ne limităm la o simplă observație – vom trece la acțiune! :)
Cum se creează o instanță a unei clase al cărei nume de clasă nu este cunoscut până când programul este executat.
Să începem cu constructorul implicit. Clasa noastrăCat
nu are încă una, așa că hai să o adăugăm:
public Cat() {
}
Iată codul pentru crearea unui Cat
obiect folosind reflectarea ( createCat()
metoda):
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());
}
}
Intrare consola:
learn.codegym.Cat
Ieșire din consolă:
Cat{name='null', age=0}
Aceasta nu este o eroare: valorile lui name
și age
sunt afișate pe consolă deoarece am scris cod pentru a le scoate în toString()
metoda clasei Cat
. Aici citim numele unei clase al cărei obiect îl vom crea din consolă. Programul recunoaște numele clasei al cărei obiect urmează să fie creat. Din motive de concizie, am omis codul adecvat de gestionare a excepțiilor, care ar ocupa mai mult spațiu decât exemplul în sine. Într-un program real, desigur, ar trebui să gestionați situații care implică nume introduse incorect, etc. Constructorul implicit este destul de simplu, așa că, după cum puteți vedea, este ușor să îl utilizați pentru a crea o instanță a clasei :) Folosind newInstance()
metoda , creăm un nou obiect din această clasă. Este o altă chestiune dacăCat
constructorul preia argumente ca intrare. Să eliminăm constructorul implicit al clasei și să încercăm să rulăm din nou codul nostru.
null
java.lang.InstantiationException: learn.codegym.Cat
at java.lang.Class.newInstance(Class.java:427)
Ceva n-a mers bine! Am primit o eroare deoarece am apelat o metodă pentru a crea un obiect folosind constructorul implicit. Dar acum nu avem un astfel de constructor. Deci, atunci când newInstance()
metoda rulează, mecanismul de reflectare folosește vechiul nostru constructor cu doi parametri:
public Cat(String name, int age) {
this.name = name;
this.age = age;
}
Dar nu am făcut nimic cu parametrii, de parcă i-am fi uitat cu totul! Folosirea reflecției pentru a transmite argumente către constructor necesită puțină „creativitate”:
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());
}
}
Ieșire din consolă:
Cat{name='Fluffy', age=6}
Să aruncăm o privire mai atentă la ceea ce se întâmplă în programul nostru. Am creat o serie de Class
obiecte.
Class[] catClassParams = {String.class, int.class};
Ele corespund parametrilor constructorului nostru (care are doar String
și int
parametri). Le trecem la clazz.getConstructor()
metodă și obținem acces la constructorul dorit. După aceea, tot ce trebuie să facem este să apelăm newInstance()
metoda cu argumentele necesare și să nu uităm să aruncăm în mod explicit obiectul la tipul dorit: Cat
.
cat = (Cat) clazz.getConstructor(catClassParams).newInstance("Fluffy", 6);
Acum obiectul nostru este creat cu succes! Ieșire din consolă:
Cat{name='Fluffy', age=6}
Mișcându-ne imediat :)
Cum să obțineți și să setați valoarea unui câmp de instanță după nume.
Imaginează-ți că folosești o clasă scrisă de un alt programator. În plus, nu aveți posibilitatea de a-l edita. De exemplu, o bibliotecă de clasă gata făcută, ambalată într-un JAR. Puteți citi codul claselor, dar nu îl puteți schimba. Să presupunem că programatorul care a creat una dintre clasele din această bibliotecă (să fieCat
clasa noastră veche), nereușind să doarmă suficient în noaptea dinaintea finalizării designului, a eliminat getter-ul și setter-ul pentru câmp age
. Acum această clasă a venit la tine. Îndeplinește toate nevoile dvs., deoarece aveți nevoie doar Cat
de obiecte în programul dvs. Dar ai nevoie de ei să aibă un age
câmp! Aceasta este o problemă: nu putem ajunge pe teren, pentru că areprivate
modificatorul, iar getter-ul și setter-ul au fost șterse de către dezvoltatorul lipsit de somn care a creat clasa :/ Ei bine, reflecția ne poate ajuta în această situație! Avem acces la codul pentru Cat
clasă, așa că putem afla măcar ce câmpuri are și cum se numesc. Înarmați cu aceste informații, ne putem rezolva 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());
}
}
După cum se spune în comentarii, totul cu câmpul name
este simplu, deoarece dezvoltatorii clasei au furnizat un setter. Știți deja cum să creați obiecte din constructori impliciti: avem newInstance()
pentru asta. Dar va trebui să facem câteva modificări cu al doilea câmp. Să ne dăm seama ce se întâmplă aici :)
Field age = clazz.getDeclaredField("age");
Aici, folosind Class clazz
obiectul nostru, accesăm age
câmpul prin getDeclaredField()
metoda. Ne permite să obținem câmpul de vârstă ca Field age
obiect. Dar acest lucru nu este suficient, pentru că nu putem pur și simplu să atribuim valori private
câmpurilor. Pentru a face acest lucru, trebuie să facem câmpul accesibil utilizând setAccessible()
metoda:
age.setAccessible(true);
Odată ce facem acest lucru unui câmp, putem atribui o valoare:
age.set(cat, 6);
După cum puteți vedea, Field age
obiectul nostru are un fel de setter din interior spre exterior căruia îi transmitem o valoare int și obiectul al cărui câmp urmează să fie alocat. Ne rulăm main()
metoda și vedem:
Cat{name='Fluffy', age=6}
Excelent! Am reusit! :) Sa vedem ce mai putem face...
Cum să apelați o metodă de instanță după nume.
Să schimbăm puțin situația din exemplul anterior. Să presupunem căCat
dezvoltatorul clasei nu a făcut o greșeală cu getters și setters. Totul este în regulă în acest sens. Acum problema este alta: există o metodă de care avem nevoie cu siguranță, dar dezvoltatorul a făcut-o privată:
private void sayMeow() {
System.out.println("Meow!");
}
Aceasta înseamnă că, dacă creăm Cat
obiecte în programul nostru, atunci nu vom putea apela sayMeow()
metoda pe ele. Vom avea pisici care nu miauna? E ciudat :/ Cum am remedia asta? Încă o dată, Reflection API ne ajută! Știm numele metodei de care avem nevoie. Orice altceva este o tehnică:
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();
}
}
Aici facem cam același lucru pe care l-am făcut atunci când accesam un câmp privat. În primul rând, obținem metoda de care avem nevoie. Este încapsulat într-un Method
obiect:
Method sayMeow = clazz.getDeclaredMethod("sayMeow");
Metoda getDeclaredMethod()
ne permite să ajungem la metode private. Apoi, facem metoda apelabilă:
sayMeow.setAccessible(true);
Și, în sfârșit, numim metoda pe obiectul dorit:
sayMeow.invoke(cat);
Aici, apelul nostru de metodă arată ca un „callback”: suntem obișnuiți să folosim un punct pentru a îndrepta un obiect către metoda dorită ( ) cat.sayMeow()
, dar când lucrăm cu reflexie, trecem la metodă obiectul pe care vrem să îl apelăm. acea metoda. Ce este pe consola noastră?
Meow!
Totul a funcționat! :) Acum puteți vedea posibilitățile vaste pe care ni le oferă mecanismul de reflexie al Java. În situații dificile și neașteptate (cum ar fi exemplele noastre cu o clasă dintr-o bibliotecă închisă), ne poate ajuta foarte mult. Dar, ca orice mare putere, aduce o mare responsabilitate. Dezavantajele reflecției sunt descrise într-o secțiune specială de pe site-ul Oracle. Există trei dezavantaje principale:
-
Performanța este mai proastă. Metodele numite folosind reflexie au performanțe mai slabe decât metodele numite în mod normal.
-
Există restricții de securitate. Mecanismul de reflectare ne permite să schimbăm comportamentul unui program în timpul rulării. Dar la locul tău de muncă, atunci când lucrezi la un proiect real, te poți confrunta cu limitări care nu permit acest lucru.
-
Risc de expunere a informațiilor interne. Este important să înțelegem că reflecția este o încălcare directă a principiului încapsulării: ne permite să accesăm câmpuri private, metode etc. Nu cred că trebuie să menționez că ar trebui recursă la o încălcare directă și flagrantă a principiilor POO. doar în cazurile cele mai extreme, când nu există alte modalități de a rezolva o problemă din motive independente de voința dumneavoastră.
GO TO FULL VERSION