CodeGym /Blog Java /Aleatoriu /Exemple de reflecție
John Squirrels
Nivel
San Francisco

Exemple de reflecție

Publicat în grup
Poate ai întâlnit conceptul de „reflecție” în viața obișnuită. Acest cuvânt se referă de obicei la procesul de a se studia pe sine. În programare, are o semnificație similară - este un mecanism pentru analiza datelor despre un program și chiar modificarea structurii și comportamentului unui program, în timp ce programul rulează. Exemple de reflecție - 1 Important aici este că facem asta în timpul execuției, nu în timpul compilării. Dar de ce să examinăm codul în timpul execuției? La urma urmei, puteți deja să citiți codul :/ Există un motiv pentru care ideea de reflecție poate să nu fie imediat clară: până în acest moment, ați știut întotdeauna cu ce clase lucrați. De exemplu, puteți scrie o Catclasă:

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 Animalclasă părinte pentru comoditate. Mai devreme, chiar am creat o clasă reprezentând o clinică veterinară, la care puteam trece un Animalobiect (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 Catobiect 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 CodeAnalyzerclasă 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ă.
Va arata cam asa:

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:
  1. Identificarea/determinarea clasei unui obiect.
  2. Obțineți informații despre modificatorii de clasă, câmpuri, metode, constante, constructori și superclase.
  3. Aflați ce metode aparțin unei interfețe implementate.
  4. Creați o instanță a unei clase al cărei nume de clasă nu este cunoscut până când programul este executat.
  5. Obțineți și setați valoarea unui câmp de instanță după nume.
  6. Apelați o metodă de instanță după nume.
Lista impresionantă, nu? :) Notă:mecanismul de reflexie poate face toate aceste lucruri „din mers”, indiferent de tipul de obiect pe care îl transmitem analizorului nostru de cod! Să explorăm capacitățile API-ului Reflection, analizând câteva exemple.

Cum se identifică/determină clasa unui obiect

Să începem cu elementele de bază. Punctul de intrare în motorul de reflecție Java este clasa Class. 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 Catclasa î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ă Animalclasă părinte:

package learn.codegym;
public class Animal {

   private String name;
   private int age;
}
Și vom face ca Catclasa 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ă: privatevariabilele 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ă Catnu are încă una, așa că hai să o adăugăm:

public Cat() {
  
}
Iată codul pentru crearea unui Catobiect 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 agesunt 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. Exemple de reflecție - 3Din 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ăCatconstructorul 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 Classobiecte.

Class[] catClassParams = {String.class, int.class};
Ele corespund parametrilor constructorului nostru (care are doar Stringși intparametri). 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ă fie Catclasa 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 Catde obiecte în programul dvs. Dar ai nevoie de ei să aibă un agecâmp! Aceasta este o problemă: nu putem ajunge pe teren, pentru că areprivatemodificatorul, 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 Catclasă, 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 nameeste 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 clazzobiectul nostru, accesăm agecâmpul prin getDeclaredField()metoda. Ne permite să obținem câmpul de vârstă ca Field ageobiect. Dar acest lucru nu este suficient, pentru că nu putem pur și simplu să atribuim valori privatecâ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 ageobiectul 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ă Catdezvoltatorul 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 Catobiecte î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 Methodobiect:

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:
  1. Performanța este mai proastă. Metodele numite folosind reflexie au performanțe mai slabe decât metodele numite în mod normal.

  2. 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.

  3. 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ă.

Folosește reflecția cu înțelepciune și numai în situațiile în care nu poate fi evitată și nu uita de neajunsurile ei. Cu aceasta, lecția noastră s-a încheiat. S-a dovedit a fi destul de lung, dar ai învățat multe astăzi :)
Comentarii
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION