CodeGym /Java blogg /Slumpmässig /Exempel på reflektion
John Squirrels
Nivå
San Francisco

Exempel på reflektion

Publicerad i gruppen
Kanske har du stött på begreppet "reflektion" i det vanliga livet. Detta ord syftar vanligtvis på processen att studera sig själv. I programmering har det en liknande betydelse - det är en mekanism för att analysera data om ett program, och till och med ändra strukturen och beteendet hos ett program, medan programmet körs. Exempel på reflektion - 1 Det viktiga här är att vi gör detta vid körning, inte vid kompilering. Men varför undersöka koden vid körning? När allt kommer omkring kan du redan läsa koden :/ Det finns en anledning till att tanken på reflektion kanske inte är direkt tydlig: fram till denna punkt visste du alltid vilka klasser du arbetade med. Du kan till exempel skriva en Catklass:

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 allt om det, och du kan se de områden och metoder som det har. Anta att du plötsligt behöver introducera andra djurklasser i programmet. Du kan förmodligen skapa en klassarvsstruktur med en Animalöverordnad klass för enkelhetens skull. Tidigare skapade vi till och med en klass som representerade en veterinärklinik, till vilken vi kunde skicka ett Animalobjekt (instans av en föräldraklass), och programmet behandlade djuret på lämpligt sätt baserat på om det var en hund eller en katt. Även om detta inte är de enklaste uppgifterna, kan programmet lära sig all nödvändig information om klasserna vid kompileringstillfället. Följaktligen, när du skickar ett Catobjekt till metoderna för veterinärkliniken klass imain()metoden vet programmet redan att det är en katt, inte en hund. Låt oss nu föreställa oss att vi står inför en annan uppgift. Vårt mål är att skriva en kodanalysator. Vi måste skapa en CodeAnalyzerklass med en enda metod: void analyzeObject(Object o). Denna metod bör:
  • bestämma klassen för objektet som skickas till det och visa klassnamnet på konsolen;
  • bestämma namnen på alla fält i den godkända klassen, inklusive privata, och visa dem på konsolen;
  • bestäm namnen på alla metoder i den godkända klassen, inklusive privata, och visa dem på konsolen.
Det kommer att se ut ungefär så här:

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 tydligt se hur denna uppgift skiljer sig från andra uppgifter som du har löst tidigare. Med vårt nuvarande mål ligger svårigheten i det faktum att varken vi eller programmet vet exakt vad som kommer att skickas tillanalyzeClass()metod. Om du skriver ett sådant program kommer andra programmerare att börja använda det, och de kan skicka vad som helst till den här metoden - vilken standard Java-klass som helst eller vilken annan klass de skriver. Den godkända klassen kan ha valfritt antal variabler och metoder. Med andra ord, vi (och vårt program) har ingen aning om vilka klasser vi kommer att arbeta med. Men ändå måste vi slutföra denna uppgift. Och det är här som standard Java Reflection API kommer till vår hjälp. Reflection API är ett kraftfullt verktyg för språket. Oracles officiella dokumentation rekommenderar att denna mekanism endast ska användas av erfarna programmerare som vet vad de gör. Du kommer snart att förstå varför vi ger den här typen av varningar i förväg :) Här är en lista över saker du kan göra med Reflection API:
  1. Identifiera/bestäm klassen för ett objekt.
  2. Få information om klassmodifierare, fält, metoder, konstanter, konstruktorer och superklasser.
  3. Ta reda på vilka metoder som hör till ett eller flera implementerade gränssnitt.
  4. Skapa en instans av en klass vars klassnamn inte är känt förrän programmet körs.
  5. Hämta och ställ in värdet på ett instansfält efter namn.
  6. Kalla en instansmetod vid namn.
Imponerande lista, va? :) Notera:reflektionsmekanismen kan göra allt det här "i farten", oavsett vilken typ av objekt vi skickar till vår kodanalysator! Låt oss utforska Reflection API:s möjligheter genom att titta på några exempel.

Hur man identifierar/bestämmer klassen för ett objekt

Låt oss börja med grunderna. Ingångspunkten till Java-reflektionsmotorn är Classklassen. Ja, det ser riktigt roligt ut, men det är vad reflektion är :) Med hjälp av Classklassen bestämmer vi först klassen för alla objekt som skickas till vår metod. Låt oss försöka göra det här:

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));
   }
}
Konsolutgång:

class learn.codegym.Cat
Var uppmärksam på två saker. Först lägger vi medvetet Catklassen i ett separat learn.codegympaket. Nu kan du se att getClass()metoden returnerar klassens fullständiga namn. För det andra döpte vi vår variabel clazz. Det ser lite konstigt ut. Det skulle vara vettigt att kalla det "klass", men "klass" är ett reserverat ord i Java. Kompilatorn tillåter inte att variabler kallas så. Vi var tvungna att komma runt det på något sätt :) Inte illa till att börja med! Vad mer hade vi på den listan över förmågor?

Hur man får information om klassmodifierare, fält, metoder, konstanter, konstruktorer och superklasser.

Nu blir allt mer intressant! I den aktuella klassen har vi inga konstanter eller en överordnad klass. Låt oss lägga till dem för att skapa en komplett bild. Skapa den enklaste Animalföräldraklassen:

package learn.codegym;
public class Animal {

   private String name;
   private int age;
}
Och vi kommer att få vår Catklass att ärva Animaloch lägga till 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 hela bilden! Låt oss se vad reflektion är kapabel till :)

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));
   }
}
Här är vad vi ser på konsolen:

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)]
Titta på all den detaljerade klassinformationen som vi kunde få! Och inte bara offentlig information, utan även privat information! Notera: privatevariabler visas också i listan. Vår "analys" av klassen kan anses vara i stort sett fullständig: vi använder analyzeObject()metoden för att lära oss allt vi kan. Men det här är inte allt vi kan göra med reflektion. Vi är inte begränsade till enkel observation – vi går vidare till att vidta åtgärder! :)

Hur man skapar en instans av en klass vars klassnamn inte är känt förrän programmet körs.

Låt oss börja med standardkonstruktorn. Vår Catklass har ingen ännu, så låt oss lägga till den:

public Cat() {
  
}
Här är koden för att skapa ett Catobjekt med reflektion ( createCat()metod):

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());
   }
}
Konsolingång:

learn.codegym.Cat
Konsolutgång:

Cat{name='null', age=0}
Detta är inte ett fel: värdena för nameoch agevisas på konsolen eftersom vi skrev kod för att mata ut dem i klassens toString()metod . CatHär läser vi namnet på en klass vars objekt vi kommer att skapa från konsolen. Programmet känner igen namnet på den klass vars objekt ska skapas. Exempel på reflektion - 3För korthetens skull utelämnade vi korrekt undantagshanteringskod, som skulle ta mer utrymme än själva exemplet. I ett riktigt program ska du förstås hantera situationer som involverar felaktigt inmatade namn etc. Standardkonstruktorn är ganska enkel, så som du kan se är det lätt att använda den för att skapa en instans av klassen :) Använda newInstance()metoden skapar vi ett nytt objekt av den här klassen. Det är en annan sak omCatkonstruktorn tar argument som input. Låt oss ta bort klassens standardkonstruktor och försöka köra vår kod igen.

null
java.lang.InstantiationException: learn.codegym.Cat 
at java.lang.Class.newInstance(Class.java:427)
Något gick fel! Vi fick ett fel eftersom vi anropade en metod för att skapa ett objekt med standardkonstruktorn. Men vi har ingen sådan konstruktör nu. Så när newInstance()metoden körs använder reflektionsmekanismen vår gamla konstruktor med två parametrar:

public Cat(String name, int age) {
   this.name = name;
   this.age = age;
}
Men vi gjorde ingenting med parametrarna, som om vi helt hade glömt bort dem! Att använda reflektion för att skicka argument till konstruktören kräver lite "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());
   }
}
Konsolutgång:

Cat{name='Fluffy', age=6}
Låt oss ta en närmare titt på vad som händer i vårt program. Vi skapade en rad Classobjekt.

Class[] catClassParams = {String.class, int.class};
De motsvarar parametrarna för vår konstruktör (som bara har Stringoch intparametrar). Vi skickar dem till clazz.getConstructor()metoden och får tillgång till önskad konstruktor. Efter det behöver vi bara anropa newInstance()metoden med de nödvändiga argumenten, och glöm inte att explicit casta objektet till önskad typ: Cat.

cat = (Cat) clazz.getConstructor(catClassParams).newInstance("Fluffy", 6);
Nu har vårt objekt skapats framgångsrikt! Konsolutgång:

Cat{name='Fluffy', age=6}
Går direkt :)

Hur man hämtar och ställer in värdet på ett instansfält efter namn.

Föreställ dig att du använder en klass skriven av en annan programmerare. Dessutom har du inte möjlighet att redigera den. Till exempel ett färdigt klassbibliotek paketerat i en JAR. Du kan läsa koden för klasserna, men du kan inte ändra den. Anta att programmeraren som skapade en av klasserna i det här biblioteket (låt det vara vår gamla Catklass), som inte fick tillräckligt med sömn natten innan designen slutfördes, tog bort getter och seter för fältet age. Nu har den här klassen kommit till dig. Det uppfyller alla dina behov, eftersom du bara behöver Catobjekt i ditt program. Men du behöver dem för att ha ett agefält! Detta är ett problem: vi kan inte nå fältet, eftersom det harprivatemodifierare, och getter och setter raderades av den sömnberövade utvecklaren som skapade klassen :/ Tja, reflektion kan hjälpa oss i den här situationen! Vi har tillgång till koden för Catklassen, så vi kan åtminstone ta reda på vilka fält den har och vad de heter. Med denna information kan vi lösa vårt 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 sägs i kommentarerna nameär allt med fältet okomplicerat, eftersom klassutvecklarna gav en uppsättare. Du vet redan hur man skapar objekt från standardkonstruktörer: vi har newInstance()för detta. Men vi måste pyssla lite med det andra fältet. Låt oss ta reda på vad som händer här :)

Field age = clazz.getDeclaredField("age");
Här, med hjälp av vårt Class clazzobjekt , kommer vi åt agefältet via getDeclaredField()metoden. Det låter oss få åldersfältet som ett Field ageobjekt. Men detta räcker inte, eftersom vi inte bara kan tilldela värden till privatefält. För att göra detta måste vi göra fältet tillgängligt genom att använda setAccessible()metoden:

age.setAccessible(true);
När vi gör detta till ett fält kan vi tilldela ett värde:

age.set(cat, 6);
Som du kan se har vårt Field ageobjekt en slags inifrån-ut-sättare till vilken vi skickar ett int-värde och det objekt vars fält ska tilldelas. Vi kör vår main()metod och ser:

Cat{name='Fluffy', age=6}
Excellent! Vi gjorde det! :) Låt oss se vad mer vi kan göra...

Hur man kallar en instansmetod vid namn.

Låt oss ändra situationen något i föregående exempel. Låt oss säga att Catklassutvecklaren inte gjorde ett misstag med getters och seters. Allt är okej i det avseendet. Nu är problemet ett annat: det finns en metod som vi definitivt behöver, men utvecklaren gjorde den privat:

private void sayMeow() {

   System.out.println("Meow!");
}
Det betyder att om vi skapar Catobjekt i vårt program kommer vi inte att kunna anropa sayMeow()metoden på dem. Kommer vi att ha katter som inte jamar? Det är konstigt :/ Hur skulle vi fixa detta? Återigen hjälper Reflection API oss! Vi vet namnet på metoden vi behöver. Allt annat är 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();
   }
}
Här gör vi mycket av samma sak som vi gjorde när vi fick tillgång till ett privat fält. Först får vi den metod vi behöver. Det är inkapslat i ett Methodobjekt:

Method sayMeow = clazz.getDeclaredMethod("sayMeow");
Metoden getDeclaredMethod()låter oss komma till privata metoder. Därefter gör vi metoden anropbar:

sayMeow.setAccessible(true);
Och slutligen kallar vi metoden på det önskade objektet:

sayMeow.invoke(cat);
Här ser vårt metodanrop ut som ett "återuppringning": vi är vana vid att använda en punkt för att peka ett objekt på den önskade metoden ( ), cat.sayMeow()men när vi arbetar med reflektion överför vi det objekt som vi vill anropa till metoden. den metoden. Vad finns på vår konsol?

Meow!
Allt fungerade! :) Nu kan du se de stora möjligheter som Javas reflektionsmekanism ger oss. I svåra och oväntade situationer (som våra exempel med en klass från ett stängt bibliotek) kan det verkligen hjälpa oss mycket. Men som med vilken stormakt som helst, medför det stort ansvar. Nackdelarna med reflektion beskrivs i ett särskilt avsnitt på Oracles hemsida. Det finns tre huvudsakliga nackdelar:
  1. Prestanda är sämre. Metoder som kallas att använda reflektion har sämre prestanda än metoder som kallas på vanligt sätt.

  2. Det finns säkerhetsrestriktioner. Reflexionsmekanismen låter oss ändra ett programs beteende under körning. Men på din arbetsplats, när du arbetar med ett riktigt projekt, kan du möta begränsningar som inte tillåter detta.

  3. Risk för exponering av intern information. Det är viktigt att förstå att reflektion är ett direkt brott mot principen om inkapsling: den ger oss tillgång till privata områden, metoder etc. Jag tror inte att jag behöver nämna att ett direkt och flagrant brott mot principerna för OOP bör tillgripas endast i de mest extrema fallen, när det inte finns några andra sätt att lösa ett problem av skäl utanför din kontroll.

Använd reflektion klokt och endast i situationer där den inte kan undvikas, och glöm inte bort dess brister. Med detta har vår lektion kommit till sitt slut. Det blev ganska långt, men du lärde dig mycket idag :)
Kommentarer
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION