CodeGym /Java blog /Tilfældig /Eksempler på refleksion
John Squirrels
Niveau
San Francisco

Eksempler på refleksion

Udgivet i gruppen
Måske er du stødt på begrebet "refleksion" i det almindelige liv. Dette ord refererer normalt til processen med at studere sig selv. I programmering har det en lignende betydning - det er en mekanisme til at analysere data om et program og endda ændre strukturen og adfærden af ​​et program, mens programmet kører. Eksempler på refleksion - 1 Det vigtige her er, at vi gør dette på runtime, ikke på kompileringstidspunktet. Men hvorfor undersøge koden under kørsel? Du kan jo allerede læse koden :/ Der er en grund til, at tanken om refleksion måske ikke umiddelbart er klar: Indtil nu vidste du altid, hvilke klasser du arbejdede med. For eksempel kan du skrive en Catklasse:

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 +
           '}';
}

}
Du ved alt om det, og du kan se de felter og metoder, som det har. Antag, at du pludselig skal introducere andre dyreklasser til programmet. Du kunne sandsynligvis oprette en klassearvstruktur med en Animaloverordnet klasse for nemheds skyld. Tidligere har vi endda oprettet en klasse, der repræsenterer en veterinærklinik, hvortil vi kunne videregive et Animalobjekt (forekomst af en forældreklasse), og programmet behandlede dyret passende ud fra, om det var en hund eller en kat. Selvom dette ikke er de enkleste opgaver, er programmet i stand til at lære alle de nødvendige oplysninger om klasserne på kompileringstidspunktet. Derfor, når du videregiver en Catgenstand til metoderne i veterinærklinikkens klasse imain()metode, ved programmet allerede, at det er en kat, ikke en hund. Lad os nu forestille os, at vi står over for en anden opgave. Vores mål er at skrive en kodeanalysator. Vi skal oprette en CodeAnalyzerklasse med en enkelt metode: void analyzeObject(Object o). Denne metode skal:
  • bestemme klassen for det objekt, der er sendt til det, og vis klassenavnet på konsollen;
  • bestemme navnene på alle felter i den beståede klasse, inklusive private, og vis dem på konsollen;
  • bestemme navnene på alle metoder i den beståede klasse, inklusive private, og vis dem på konsollen.
Det kommer til at se sådan ud:

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
   }
  
}
Nu kan vi tydeligt se, hvordan denne opgave adskiller sig fra andre opgaver, som du tidligere har løst. Med vores nuværende mål ligger vanskeligheden i, at hverken vi eller programmet ved, hvad der præcist vil blive videregivet tilanalyzeClass()metode. Hvis du skriver et sådant program, vil andre programmører begynde at bruge det, og de kan overføre hvad som helst til denne metode - enhver standard Java-klasse eller enhver anden klasse, de skriver. Den beståede klasse kan have et vilkårligt antal variabler og metoder. Med andre ord har vi (og vores program) ingen idé om, hvilke klasser vi skal arbejde med. Men alligevel skal vi fuldføre denne opgave. Og det er her, standard Java Reflection API kommer os til hjælp. Reflection API er et kraftfuldt sprogværktøj. Oracles officielle dokumentation anbefaler, at denne mekanisme kun bør bruges af erfarne programmører, der ved, hvad de laver. Du vil snart forstå, hvorfor vi giver denne form for advarsel på forhånd :) Her er en liste over ting, du kan gøre med Reflection API:
  1. Identificer/bestem klassen af ​​et objekt.
  2. Få information om klassemodifikatorer, felter, metoder, konstanter, konstruktører og superklasser.
  3. Find ud af, hvilke metoder der hører til en eller flere implementerede grænseflader.
  4. Opret en forekomst af en klasse, hvis klassenavn ikke kendes, før programmet er afviklet.
  5. Hent og indstil værdien af ​​et forekomstfelt efter navn.
  6. Kald en instansmetode ved navn.
Imponerende liste, ikke? :) Bemærk:reflektionsmekanismen kan gøre alt dette "on the fly", uanset hvilken type objekt vi sender til vores kodeanalysator! Lad os udforske Reflection API's muligheder ved at se på nogle eksempler.

Hvordan man identificerer/bestemmer klassen af ​​et objekt

Lad os starte med det grundlæggende. Indgangspunktet til Java-reflektionsmotoren er klassen Class. Ja, det ser virkelig sjovt ud, men det er det, der er refleksion :) Ved hjælp af Classklassen bestemmer vi først klassen for ethvert objekt, der sendes til vores metode. Lad os prøve at gøre dette:

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));
   }
}
Konsoludgang:

class learn.codegym.Cat
Vær opmærksom på to ting. Først lægger vi bevidst Catklassen i en separat learn.codegympakke. Nu kan du se, at getClass()metoden returnerer det fulde navn på klassen. For det andet navngav vi vores variabel clazz. Det ser lidt mærkeligt ud. Det ville give mening at kalde det "klasse", men "klasse" er et reserveret ord i Java. Compileren tillader ikke at variable kaldes det. Det måtte vi på en eller anden måde komme uden om :) Ikke dårligt til at begynde med! Hvad havde vi ellers på listen over kapaciteter?

Sådan får du information om klassemodifikatorer, felter, metoder, konstanter, konstruktører og superklasser.

Nu bliver tingene mere interessante! I den nuværende klasse har vi ingen konstanter eller en overordnet klasse. Lad os tilføje dem for at skabe et komplet billede. Opret den enkleste Animalforældreklasse:

package learn.codegym;
public class Animal {

   private String name;
   private int age;
}
Og vi får vores Catklasse til at arve Animalog tilføje en konstant:

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
}
Nu har vi det komplette billede! Lad os se, hvad refleksion er i stand til :)

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));
   }
}
Her er, hvad vi ser på konsollen:

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)]
Se på al den detaljerede klasseinformation, som vi var i stand til at få! Og ikke kun offentlig information, men også privat information! Bemærk: privatevariabler vises også på listen. Vores "analyse" af klassen kan betragtes som i det væsentlige komplet: vi bruger metoden analyzeObject()til at lære alt, hvad vi kan. Men det er ikke alt, vi kan gøre med refleksion. Vi er ikke begrænset til simpel observation – vi går videre til handling! :)

Hvordan man opretter en instans af en klasse, hvis klassenavn ikke er kendt, før programmet er afviklet.

Lad os starte med standardkonstruktøren. Vores Catklasse har ikke en endnu, så lad os tilføje den:

public Cat() {
  
}
Her er koden til at oprette et Catobjekt ved hjælp af refleksion ( createCat()metode):

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());
   }
}
Konsol input:

learn.codegym.Cat
Konsoludgang:

Cat{name='null', age=0}
Dette er ikke en fejl: værdierne af nameog agevises på konsollen, fordi vi skrev kode for at udlæse dem i toString()klassens metode Cat. Her læser vi navnet på en klasse, hvis objekt vi vil oprette fra konsollen. Programmet genkender navnet på den klasse, hvis objekt skal oprettes. Eksempler på refleksion - 3For korthedens skyld udelod vi korrekt undtagelseshåndteringskode, som ville optage mere plads end selve eksemplet. I et rigtigt program skal du selvfølgelig håndtere situationer, der involverer forkert indtastede navne osv. Standardkonstruktøren er ret simpel, så som du kan se, er det nemt at bruge den til at oprette en instans af klassen :) Ved hjælp af newInstance()metoden , opretter vi et nyt objekt af denne klasse. Det er en anden sag, hvisCatkonstruktør tager argumenter som input. Lad os fjerne klassens standardkonstruktør og prøve at køre vores kode igen.

null
java.lang.InstantiationException: learn.codegym.Cat 
at java.lang.Class.newInstance(Class.java:427)
Noget gik galt! Vi fik en fejl, fordi vi kaldte en metode til at oprette et objekt ved hjælp af standardkonstruktøren. Men sådan en konstruktør har vi ikke nu. Så når newInstance()metoden kører, bruger refleksionsmekanismen vores gamle konstruktør med to parametre:

public Cat(String name, int age) {
   this.name = name;
   this.age = age;
}
Men vi gjorde ikke noget med parametrene, som om vi helt havde glemt dem! At bruge refleksion til at videregive argumenter til konstruktøren kræver lidt "kreativitet":

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());
   }
}
Konsoludgang:

Cat{name='Fluffy', age=6}
Lad os se nærmere på, hvad der sker i vores program. Vi skabte en række Classobjekter.

Class[] catClassParams = {String.class, int.class};
De svarer til parametrene for vores konstruktør (som bare har Stringog intparametre). Vi videregiver dem til clazz.getConstructor()metoden og får adgang til den ønskede konstruktør. Derefter skal vi bare kalde newInstance()metoden med de nødvendige argumenter, og glem ikke eksplicit at caste objektet til den ønskede type: Cat.

cat = (Cat) clazz.getConstructor(catClassParams).newInstance("Fluffy", 6);
Nu er vores objekt skabt med succes! Konsoludgang:

Cat{name='Fluffy', age=6}
Kører lige med :)

Sådan hentes og indstilles værdien af ​​et instansfelt efter navn.

Forestil dig, at du bruger en klasse skrevet af en anden programmør. Derudover har du ikke mulighed for at redigere den. For eksempel et færdiglavet klassebibliotek pakket i en JAR. Du kan læse klassernes kode, men du kan ikke ændre den. Antag, at programmøren, der oprettede en af ​​klasserne i dette bibliotek (lad det være vores gamle Catklasse), og ikke fik nok søvn natten før designet blev færdiggjort, fjernede getter og setter for feltet age. Nu er denne klasse kommet til dig. Det opfylder alle dine behov, da du kun skal bruge Catobjekter i dit program. Men du har brug for, at de har et agefelt! Dette er et problem: Vi kan ikke nå feltet, fordi det harprivatemodifier, og getter og setter blev slettet af den søvnberøvede udvikler, der oprettede klassen :/ Nå, refleksion kan hjælpe os i denne situation! Vi har adgang til koden til Catklassen, så vi i det mindste kan finde ud af hvilke felter den har og hvad de hedder. Med disse oplysninger kan vi løse vores problem:

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());
   }
}
Som det fremgår af kommentarerne, nameer alt med feltet ligetil, da klasseudviklerne leverede en opstiller. Du ved allerede, hvordan du opretter objekter fra standardkonstruktører: vi har newInstance()til dette. Men vi bliver nødt til at pille ved det andet felt. Lad os finde ud af hvad der sker her :)

Field age = clazz.getDeclaredField("age");
Her Class clazzfår vi ved hjælp af vores objekt adgang til agefeltet via getDeclaredField()metoden. Det lader os få aldersfeltet som et Field ageobjekt. Men det er ikke nok, for vi kan ikke bare tildele værdier til privatefelter. For at gøre dette skal vi gøre feltet tilgængeligt ved at bruge setAccessible()metoden:

age.setAccessible(true);
Når vi har gjort dette til et felt, kan vi tildele en værdi:

age.set(cat, 6);
Som du kan se, har vores Field ageobjekt en slags inside-out-sætter, hvortil vi sender en int-værdi og det objekt, hvis felt skal tildeles. Vi kører vores main()metode og ser:

Cat{name='Fluffy', age=6}
Fremragende! Vi gjorde det! :) Lad os se, hvad vi ellers kan gøre...

Sådan kalder du en instansmetode ved navn.

Lad os ændre situationen lidt i det foregående eksempel. Lad os sige, at Catklasseudvikleren ikke lavede en fejl med getters og sættere. Alt er i orden i den forbindelse. Nu er problemet anderledes: der er en metode, som vi helt sikkert har brug for, men udvikleren gjorde den privat:

private void sayMeow() {

   System.out.println("Meow!");
}
Det betyder, at hvis vi opretter Catobjekter i vores program, så vil vi ikke være i stand til at kalde sayMeow()metoden på dem. Får vi katte, der ikke miaver? Det er mærkeligt :/ Hvordan fikser vi det? Endnu en gang hjælper Reflection API os! Vi kender navnet på den metode, vi skal bruge. Alt andet er en teknikalitet:

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();
   }
}
Her gør vi meget af det samme, som vi gjorde, da vi fik adgang til et privat felt. Først får vi den metode, vi har brug for. Det er indkapslet i et Methodobjekt:

Method sayMeow = clazz.getDeclaredMethod("sayMeow");
Metoden getDeclaredMethod()lader os komme til private metoder. Dernæst gør vi metoden callable:

sayMeow.setAccessible(true);
Og endelig kalder vi metoden på det ønskede objekt:

sayMeow.invoke(cat);
Her ser vores metodekald ud som et "tilbagekald": vi er vant til at bruge et punktum til at pege et objekt på den ønskede metode ( cat.sayMeow()), men når vi arbejder med refleksion, overfører vi til metoden det objekt, som vi vil kalde på den metode. Hvad er der på vores konsol?

Meow!
Alt fungerede! :) Nu kan du se de enorme muligheder, som Javas reflektionsmekanisme giver os. I svære og uventede situationer (såsom vores eksempler med en klasse fra et lukket bibliotek), kan det virkelig hjælpe os meget. Men som med enhver stor magt medfører det et stort ansvar. Ulemperne ved refleksion er beskrevet i et særligt afsnit på Oracles hjemmeside. Der er tre hovedulemper:
  1. Ydeevnen er dårligere. Metoder, der kaldes ved hjælp af refleksion, har dårligere ydeevne end metoder, der kaldes på normal vis.

  2. Der er sikkerhedsrestriktioner. Refleksionsmekanismen lader os ændre et programs adfærd under kørsel. Men på din arbejdsplads, når du arbejder på et rigtigt projekt, kan du møde begrænsninger, der ikke tillader dette.

  3. Risiko for eksponering af intern information. Det er vigtigt at forstå, at refleksion er en direkte krænkelse af princippet om indkapsling: den giver os adgang til private områder, metoder osv. Jeg tror ikke, jeg behøver at nævne, at en direkte og åbenlys krænkelse af principperne i OOP bør gribes ind. kun i de mest ekstreme tilfælde, når der ikke er andre måder at løse et problem på af årsager uden for din kontrol.

Brug refleksion klogt og kun i situationer, hvor den ikke kan undgås, og glem ikke dens mangler. Med dette er vores lektion slut. Det viste sig at være ret langt, men du lærte meget i dag :)
Kommentarer
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION