CodeGym /Java-blogg /Tilfeldig /Eksempler på refleksjon
John Squirrels
Nivå
San Francisco

Eksempler på refleksjon

Publisert i gruppen
Kanskje du har møtt begrepet "refleksjon" i det vanlige livet. Dette ordet refererer vanligvis til prosessen med å studere seg selv. I programmering har det en lignende betydning - det er en mekanisme for å analysere data om et program, og til og med endre strukturen og oppførselen til et program mens programmet kjører. Eksempler på refleksjon - 1 Det viktige her er at vi gjør dette på kjøretid, ikke på kompileringstidspunkt. Men hvorfor undersøke koden under kjøring? Tross alt kan du allerede lese koden :/ Det er en grunn til at tanken om refleksjon kanskje ikke er umiddelbart klar: frem til dette punktet visste du alltid hvilke klasser du jobbet med. Du kan for eksempel 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 vet alt om det, og du kan se feltene og metodene som det har. Tenk deg at du plutselig trenger å introdusere andre dyreklasser til programmet. Du kan sannsynligvis opprette en klassearvstruktur med en Animaloverordnet klasse for enkelhets skyld. Tidligere opprettet vi til og med en klasse som representerte en veterinærklinikk, som vi kunne sende et Animalobjekt til (forekomst av en foreldreklasse), og programmet behandlet dyret riktig basert på om det var en hund eller en katt. Selv om dette ikke er de enkleste oppgavene, er programmet i stand til å lære all nødvendig informasjon om klassene på kompileringstidspunktet. Følgelig, når du sender et Catobjekt til metodene til veterinærklinikken i klassenmain()metoden, vet programmet allerede at det er en katt, ikke en hund. La oss nå forestille oss at vi står overfor en annen oppgave. Målet vårt er å skrive en kodeanalysator. Vi må lage en CodeAnalyzerklasse med en enkelt metode: void analyzeObject(Object o). Denne metoden bør:
  • bestemme klassen til objektet som ble sendt til det og vis klassenavnet på konsollen;
  • bestemme navnene på alle feltene i den beståtte klassen, inkludert private, og vis dem på konsollen;
  • Bestem navnene på alle metodene for den beståtte klassen, inkludert private, og vis dem på konsollen.
Det vil se omtrent slik ut:

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
   }
  
}
Nå kan vi tydelig se hvordan denne oppgaven skiller seg fra andre oppgaver som du har løst tidligere. Med vårt nåværende mål ligger vanskeligheten i det faktum at verken vi eller programmet vet nøyaktig hva som vil bli sendt tilanalyzeClass()metode. Hvis du skriver et slikt program, vil andre programmerere begynne å bruke det, og de kan sende hva som helst til denne metoden - hvilken som helst standard Java-klasse eller hvilken som helst annen klasse de skriver. Den beståtte klassen kan ha et hvilket som helst antall variabler og metoder. Med andre ord, vi (og programmet vårt) har ingen anelse om hvilke klasser vi skal jobbe med. Men likevel må vi fullføre denne oppgaven. Og det er her standard Java Reflection API kommer oss til hjelp. Reflection API er et kraftig verktøy for språket. Oracles offisielle dokumentasjon anbefaler at denne mekanismen kun skal brukes av erfarne programmerere som vet hva de gjør. Du vil snart forstå hvorfor vi gir denne typen advarsel på forhånd :) Her er en liste over ting du kan gjøre med Reflection API:
  1. Identifiser/bestem klassen til et objekt.
  2. Få informasjon om klassemodifikatorer, felt, metoder, konstanter, konstruktører og superklasser.
  3. Finn ut hvilke metoder som tilhører et eller flere implementerte grensesnitt.
  4. Opprett en forekomst av en klasse hvis klassenavn ikke er kjent før programmet er kjørt.
  5. Hent og angi verdien til et forekomstfelt etter navn.
  6. Kall en forekomstmetode ved navn.
Imponerende liste, ikke sant? :) Merk:refleksjonsmekanismen kan gjøre alt dette "on the fly", uavhengig av hvilken type objekt vi sender til kodeanalysatoren vår! La oss utforske funksjonene til Reflection API ved å se på noen eksempler.

Hvordan identifisere/bestemme klassen til et objekt

La oss starte med det grunnleggende. Inngangspunktet til Java-refleksjonsmotoren er klassen Class. Ja, det ser veldig morsomt ut, men det er det som er refleksjon :) Ved å bruke klassen Classbestemmer vi først klassen til et objekt som sendes til metoden vår. La oss prøve å gjø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));
   }
}
Konsoll utgang:

class learn.codegym.Cat
Vær oppmerksom på to ting. Først legger vi bevisst Catklassen i en egen learn.codegympakke. Nå kan du se at getClass()metoden returnerer hele navnet på klassen. For det andre ga vi navnet variabelen vår clazz. Det ser litt rart ut. Det ville være fornuftig å kalle det "klasse", men "klasse" er et reservert ord i Java. Kompilatoren vil ikke tillate at variabler kalles det. Vi måtte komme rundt det på en eller annen måte :) Ikke verst for en start! Hva mer hadde vi på listen over evner?

Hvordan få informasjon om klassemodifikatorer, felt, metoder, konstanter, konstruktører og superklasser.

Nå blir ting mer interessant! I den nåværende klassen har vi ingen konstanter eller en overordnet klasse. La oss legge dem til for å lage et komplett bilde. Lag den enkleste Animalforeldreklassen:

package learn.codegym;
public class Animal {

   private String name;
   private int age;
}
Og vi får Catklassen vår til å arve Animalog legge til én 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
}
Nå har vi det komplette bildet! La oss se hva refleksjon 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 hva 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å all den detaljerte klasseinformasjonen vi var i stand til å få! Og ikke bare offentlig informasjon, men også privat informasjon! Merk: privatevariabler vises også i listen. Vår "analyse" av klassen kan i hovedsak anses som komplett: vi bruker metoden analyzeObject()for å lære alt vi kan. Men dette er ikke alt vi kan gjøre med refleksjon. Vi er ikke begrenset til enkel observasjon – vi går videre til å ta grep! :)

Hvordan lage en forekomst av en klasse hvis klassenavn ikke er kjent før programmet er kjørt.

La oss starte med standardkonstruktøren. Klassen vår Cathar ikke en ennå, så la oss legge den til:

public Cat() {
  
}
Her er koden for å lage et Catobjekt ved hjelp av refleksjon ( 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());
   }
}
Konsollinngang:

learn.codegym.Cat
Konsoll utgang:

Cat{name='null', age=0}
Dette er ikke en feil: verdiene til nameog agevises på konsollen fordi vi skrev kode for å sende dem ut i toString()metoden til Catklassen. Her leser vi navnet på en klasse hvis objekt vi skal lage fra konsollen. Programmet gjenkjenner navnet på klassen hvis objekt skal opprettes. Eksempler på refleksjon - 3For korthets skyld utelot vi riktig unntakshåndteringskode, som ville ta mer plass enn selve eksemplet. I et ekte program skal du selvfølgelig håndtere situasjoner som involverer feil oppgitte navn osv. Standardkonstruktøren er ganske enkel, så som du kan se er det enkelt å bruke den til å lage en forekomst av klassen :) Ved å bruke newInstance()metoden , lager vi et nytt objekt av denne klassen. Det er en annen sak omCatkonstruktør tar argumenter som input. La oss fjerne klassens standardkonstruktør og prøve å kjøre koden vår på nytt.

null
java.lang.InstantiationException: learn.codegym.Cat 
at java.lang.Class.newInstance(Class.java:427)
Noe gikk galt! Vi fikk en feil fordi vi kalte en metode for å lage et objekt ved å bruke standardkonstruktøren. Men vi har ikke en slik konstruktør nå. Så når newInstance()metoden kjører, bruker refleksjonsmekanismen vår gamle konstruktør med to parametere:

public Cat(String name, int age) {
   this.name = name;
   this.age = age;
}
Men vi gjorde ikke noe med parameterne, som om vi helt hadde glemt dem! Å bruke refleksjon for å sende argumenter til konstruktøren krever litt "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());
   }
}
Konsoll utgang:

Cat{name='Fluffy', age=6}
La oss se nærmere på hva som skjer i programmet vårt. Vi laget en rekke Classobjekter.

Class[] catClassParams = {String.class, int.class};
De tilsvarer parameterne til konstruktøren vår (som bare har Stringog intparametere). Vi sender dem til clazz.getConstructor()metoden og får tilgang til ønsket konstruktør. Etter det er alt vi trenger å gjøre å kalle newInstance()metoden med de nødvendige argumentene, og ikke glem å eksplisitt kaste objektet til ønsket type: Cat.

cat = (Cat) clazz.getConstructor(catClassParams).newInstance("Fluffy", 6);
Nå er objektet vårt opprettet! Konsoll utgang:

Cat{name='Fluffy', age=6}
Går rett på vei :)

Hvordan hente og angi verdien av et forekomstfelt etter navn.

Tenk deg at du bruker en klasse skrevet av en annen programmerer. I tillegg har du ikke muligheten til å redigere den. For eksempel et ferdig klassebibliotek pakket i en JAR. Du kan lese koden til klassene, men du kan ikke endre den. Anta at programmereren som opprettet en av klassene i dette biblioteket (la det være vår gamle Catklasse), og ikke fikk nok søvn natten før designet ble ferdigstilt, fjernet getter og setter for feltet age. Nå har denne timen kommet til deg. Den oppfyller alle dine behov, siden du bare trenger Catobjekter i programmet. Men du trenger at de har et agefelt! Dette er et problem: vi kan ikke nå feltet, fordi det harprivatemodifikator, og getter og setter ble slettet av den søvnberøvede utvikleren som opprettet klassen :/ Vel, refleksjon kan hjelpe oss i denne situasjonen! Vi har tilgang til koden for Catklassen, så vi kan i det minste finne ut hvilke felt den har og hva de heter. Med denne informasjonen kan vi løse problemet vårt:

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 av kommentarene, nameer alt med feltet enkelt, siden klasseutviklerne ga en setter. Du vet allerede hvordan du lager objekter fra standardkonstruktører: vi har newInstance()for dette. Men vi må fikse litt med det andre feltet. La oss finne ut hva som skjer her :)

Field age = clazz.getDeclaredField("age");
Her, ved å bruke vårt Class clazzobjekt , får vi tilgang til agefeltet via getDeclaredField()metoden. Det lar oss få aldersfeltet som et Field ageobjekt. Men dette er ikke nok, fordi vi ikke bare kan tildele verdier til privatefelt. For å gjøre dette må vi gjøre feltet tilgjengelig ved å bruke metoden setAccessible():

age.setAccessible(true);
Når vi gjør dette til et felt, kan vi tilordne en verdi:

age.set(cat, 6);
Som du kan se har objektet vårt Field ageen slags inn- og ut-setter som vi sender en int-verdi til og objektet som skal tildeles feltet. Vi kjører main()metoden vår og ser:

Cat{name='Fluffy', age=6}
Utmerket! Vi gjorde det! :) La oss se hva mer vi kan gjøre...

Hvordan kalle en instansmetode ved navn.

La oss endre litt på situasjonen i forrige eksempel. La oss si at Catklasseutvikleren ikke gjorde en feil med getterne og setterne. Alt er greit i den forbindelse. Nå er problemet annerledes: det er en metode vi definitivt trenger, men utvikleren gjorde den privat:

private void sayMeow() {

   System.out.println("Meow!");
}
Dette betyr at hvis vi lager Catobjekter i programmet vårt, vil vi ikke kunne kalle sayMeow()metoden på dem. Skal vi ha katter som ikke mjauer? Det er rart :/ Hvordan skal vi fikse dette? Nok en gang hjelper Reflection API oss! Vi vet navnet på metoden vi trenger. Alt annet er en teknisk sak:

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 gjør vi mye av det samme som vi gjorde da vi fikk tilgang til et privat felt. Først får vi metoden vi trenger. Det er innkapslet i et Methodobjekt:

Method sayMeow = clazz.getDeclaredMethod("sayMeow");
Metoden getDeclaredMethod()lar oss komme til private metoder. Deretter gjør vi metoden kallbar:

sayMeow.setAccessible(true);
Og til slutt kaller vi metoden på ønsket objekt:

sayMeow.invoke(cat);
Her ser metodekallet vårt ut som et "callback": vi er vant til å bruke et punktum for å peke et objekt på ønsket metode ( cat.sayMeow()), men når vi jobber med refleksjon, overfører vi til metoden objektet vi ønsker å kalle den metoden. Hva er på konsollen vår?

Meow!
Alt fungerte! :) Nå kan du se de enorme mulighetene som Javas refleksjonsmekanisme gir oss. I vanskelige og uventede situasjoner (som våre eksempler med en klasse fra et lukket bibliotek), kan det virkelig hjelpe oss mye. Men, som med enhver stormakt, medfører det stort ansvar. Ulempene med refleksjon er beskrevet i en spesiell del på Oracle-nettstedet. Det er tre hovedulemper:
  1. Ytelsen er dårligere. Metoder som kalles å bruke refleksjon har dårligere ytelse enn metoder som kalles på vanlig måte.

  2. Det er sikkerhetsrestriksjoner. Refleksjonsmekanismen lar oss endre et programs oppførsel under kjøring. Men på arbeidsplassen din, når du jobber med et virkelig prosjekt, kan du møte begrensninger som ikke tillater dette.

  3. Risiko for eksponering av intern informasjon. Det er viktig å forstå at refleksjon er et direkte brudd på prinsippet om innkapsling: det lar oss få tilgang til private felt, metoder osv. Jeg tror ikke jeg trenger å nevne at et direkte og åpenbart brudd på prinsippene for OOP bør benyttes kun i de mest ekstreme tilfellene, når det ikke finnes andre måter å løse et problem på av årsaker utenfor din kontroll.

Bruk refleksjon med omhu og kun i situasjoner der det ikke kan unngås, og ikke glem dens mangler. Med dette har leksjonen vår kommet til en slutt. Den ble ganske lang, men du lærte mye i dag :)
Kommentarer
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION