CodeGym /Java Blog /Willekeurig /Voorbeelden van reflectie
John Squirrels
Niveau 41
San Francisco

Voorbeelden van reflectie

Gepubliceerd in de groep Willekeurig
Misschien ben je het concept van 'reflectie' in het gewone leven tegengekomen. Dit woord verwijst meestal naar het proces van zichzelf bestuderen. Bij programmeren heeft het een vergelijkbare betekenis: het is een mechanisme voor het analyseren van gegevens over een programma en zelfs voor het wijzigen van de structuur en het gedrag van een programma terwijl het programma wordt uitgevoerd. Voorbeelden van reflectie - 1 Het belangrijkste hier is dat we dit tijdens runtime doen, niet tijdens het compileren. Maar waarom de code tijdens runtime onderzoeken? Je kunt de code immers al lezen :/ Er is een reden waarom het idee van reflectie misschien niet meteen duidelijk is: tot nu toe wist je altijd met welke klassen je werkte. U kunt bijvoorbeeld een Catklasse schrijven:

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

}
Je weet er alles van en je kunt de velden en methoden zien die het heeft. Stel dat u plotseling andere dierklassen in het programma moet introduceren. U kunt voor het gemak waarschijnlijk een klassenoverervingsstructuur maken met een Animalbovenliggende klasse. Eerder hebben we zelfs een klasse gemaakt die een dierenkliniek vertegenwoordigt, waaraan we een Animalobject (instantie van een ouderklasse) konden doorgeven, en het programma behandelde het dier op de juiste manier op basis van of het een hond of een kat was. Hoewel dit niet de eenvoudigste taken zijn, kan het programma tijdens het compileren alle nodige informatie over de klassen leren. Dienovereenkomstig, wanneer u een object doorgeeft Cataan de methoden van de veterinaire kliniekklasse in demain()methode weet het programma al dat het een kat is, geen hond. Laten we ons nu eens voorstellen dat we voor een andere taak staan. Ons doel is om een ​​code-analyzer te schrijven. We moeten een CodeAnalyzerklasse maken met een enkele methode: void analyzeObject(Object o). Deze methode moet:
  • de klasse bepalen van het object dat eraan is doorgegeven en de klassenaam weergeven op de console;
  • bepaal de namen van alle velden van de doorgegeven klasse, inclusief privé-velden, en geef ze weer op de console;
  • bepaal de namen van alle methoden van de doorgegeven klasse, inclusief privémethoden, en geef ze weer op de console.
Het zal er ongeveer zo uitzien:

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 kunnen we duidelijk zien hoe deze taak verschilt van andere taken die je eerder hebt opgelost. Met ons huidige doel ligt de moeilijkheid in het feit dat noch wij, noch het programma weten wat er precies zal worden doorgegeven aan deanalyzeClass()methode. Als je zo'n programma schrijft, gaan andere programmeurs het gebruiken en kunnen ze alles aan deze methode doorgeven - elke standaard Java-klasse of elke andere klasse die ze schrijven. De doorgegeven klasse kan een onbeperkt aantal variabelen en methoden hebben. Met andere woorden, wij (en ons programma) hebben geen idee met welke klassen we zullen werken. Maar toch moeten we deze taak voltooien. En hier komt de standaard Java Reflection API ons te hulp. De Reflection API is een krachtig hulpmiddel van de taal. De officiële documentatie van Oracle beveelt aan dat dit mechanisme alleen mag worden gebruikt door ervaren programmeurs die weten wat ze doen. Je zult snel begrijpen waarom we dit soort waarschuwingen vooraf geven :) Hier is een lijst met dingen die je kunt doen met de Reflection API:
  1. Identificeer/bepaal de klasse van een object.
  2. Krijg informatie over klassemodifiers, velden, methoden, constanten, constructors en superklassen.
  3. Zoek uit welke methodes bij een geïmplementeerde interface(s) horen.
  4. Maak een instantie van een klasse waarvan de klassenaam pas bekend is als het programma wordt uitgevoerd.
  5. Haal de waarde van een instantieveld op en stel deze in op naam.
  6. Roep een instantiemethode bij naam aan.
Indrukwekkende lijst, hè? :) Opmerking:het reflectiemechanisme kan al deze dingen "on the fly" doen, ongeacht het type object dat we doorgeven aan onze code-analysator! Laten we eens kijken naar de mogelijkheden van de Reflection API door enkele voorbeelden te bekijken.

Hoe de klasse van een object te identificeren/bepalen

Laten we beginnen met de basis. Het toegangspunt tot de Java-reflectie-engine is de Classklasse. Ja, het ziet er echt grappig uit, maar dat is wat reflectie is :) Met behulp van de Classklasse bepalen we eerst de klasse van elk object dat aan onze methode wordt doorgegeven. Laten we proberen dit te doen:

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

class learn.codegym.Cat
Let op twee dingen. Ten eerste hebben we de Catles bewust in een apart learn.codegympakket gestopt. Nu kunt u zien dat de getClass()methode de volledige naam van de klasse retourneert. Ten tweede noemden we onze variabele clazz. Dat ziet er een beetje vreemd uit. Het zou logisch zijn om het "klasse" te noemen, maar "klasse" is een gereserveerd woord in Java. De compiler staat niet toe dat variabelen zo worden genoemd. Daar moesten we op de een of andere manier omheen :) Niet slecht om te beginnen! Wat hadden we nog meer op die lijst met mogelijkheden?

Hoe u informatie kunt krijgen over klassemodificatoren, velden, methoden, constanten, constructors en superklassen.

Nu wordt het interessanter! In de huidige klasse hebben we geen constanten of een bovenliggende klasse. Laten we ze toevoegen om een ​​compleet beeld te creëren. Maak de eenvoudigste Animalbovenliggende klasse:

package learn.codegym;
public class Animal {

   private String name;
   private int age;
}
En we laten onze Catklasse erven Animalen voegen een constante toe:

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 hebben we het complete plaatje! Laten we eens kijken waar reflectie toe in staat is :)

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));
   }
}
Dit is wat we zien op de console:

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)]
Kijk eens naar al die gedetailleerde klasinformatie die we konden krijgen! En niet alleen openbare informatie, maar ook privé-informatie! Opmerking: privatevariabelen worden ook weergegeven in de lijst. Onze "analyse" van de klas kan in wezen als volledig worden beschouwd: we gebruiken de analyzeObject()methode om alles te leren wat we kunnen. Maar dit is niet alles wat we met reflectie kunnen doen. We zijn niet beperkt tot eenvoudige observatie - we gaan verder met het ondernemen van actie! :)

Een instantie maken van een klasse waarvan de klassenaam pas bekend is als het programma wordt uitgevoerd.

Laten we beginnen met de standaardconstructor. Onze Catklas heeft er nog geen, dus laten we het toevoegen:

public Cat() {
  
}
Hier is de code voor het maken van een Catobject met reflectie ( createCat()methode):

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

learn.codegym.Cat
Console-uitvoer:

Cat{name='null', age=0}
Dit is geen fout: de waarden van nameen ageworden weergegeven op de console omdat we code hebben geschreven om ze uit te voeren in de toString()methode van de Catklasse. Hier lezen we de naam van een klasse waarvan we het object zullen maken vanuit de console. Het programma herkent de naam van de klasse waarvan het object moet worden gemaakt. Voorbeelden van reflectie - 3Kortheidshalve hebben we de juiste code voor het afhandelen van uitzonderingen weggelaten, die meer ruimte zou innemen dan het voorbeeld zelf. In een echt programma moet je natuurlijk situaties afhandelen met verkeerd ingevoerde namen, enz. De standaardconstructor is vrij eenvoudig, dus zoals je kunt zien, is het gemakkelijk om hem te gebruiken om een ​​instantie van de klasse te maken newInstance():) , maken we een nieuw object van deze klasse. Het is een andere zaak als deCatconstructor neemt argumenten als invoer. Laten we de standaardconstructor van de klasse verwijderen en proberen onze code opnieuw uit te voeren.

null
java.lang.InstantiationException: learn.codegym.Cat 
at java.lang.Class.newInstance(Class.java:427)
Er is iets fout gegaan! Er is een fout opgetreden omdat we een methode hebben aangeroepen om een ​​object te maken met de standaardconstructor. Maar zo'n constructeur hebben we nu niet. Dus wanneer de newInstance()methode wordt uitgevoerd, gebruikt het reflectiemechanisme onze oude constructor met twee parameters:

public Cat(String name, int age) {
   this.name = name;
   this.age = age;
}
Maar we hebben niets met de parameters gedaan, alsof we ze helemaal vergeten waren! Reflectie gebruiken om argumenten door te geven aan de constructor vereist een beetje "creativiteit":

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

Cat{name='Fluffy', age=6}
Laten we eens nader bekijken wat er in ons programma gebeurt. We hebben een reeks Classobjecten gemaakt.

Class[] catClassParams = {String.class, int.class};
Ze komen overeen met de parameters van onze constructor (die alleen parameters Stringen heeft int). We geven ze door aan de clazz.getConstructor()methode en krijgen toegang tot de gewenste constructor. Daarna hoeven we alleen maar de newInstance()methode aan te roepen met de nodige argumenten, en vergeet niet om het object expliciet naar het gewenste type te casten: Cat.

cat = (Cat) clazz.getConstructor(catClassParams).newInstance("Fluffy", 6);
Nu is ons object succesvol gemaakt! Console-uitvoer:

Cat{name='Fluffy', age=6}
Gaat gelijk mee :)

Hoe u de waarde van een instantieveld op naam kunt krijgen en instellen.

Stel je voor dat je een klasse gebruikt die door een andere programmeur is geschreven. Bovendien heb je niet de mogelijkheid om het te bewerken. Bijvoorbeeld een kant-en-klare klassenbibliotheek verpakt in een JAR. Je kunt de code van de klassen lezen, maar niet wijzigen. Stel dat de programmeur die een van de klassen in deze bibliotheek heeft gemaakt (laat het onze oude Catklasse zijn), niet genoeg heeft geslapen op de avond voordat het ontwerp werd afgerond, de getter en setter voor het ageveld heeft verwijderd. Nu is deze klas naar jou toe gekomen. Het voldoet aan al uw behoeften, aangezien u alleen Catobjecten in uw programma nodig heeft. Maar je hebt ze nodig om een age​​veld te hebben! Dit is een probleem: we kunnen het veld niet bereiken, omdat het deprivatemodifier, en de getter en setter zijn verwijderd door de ontwikkelaar met slaapgebrek die de klasse heeft gemaakt :/ Nou, reflectie kan ons in deze situatie helpen! We hebben toegang tot de code voor de Catklasse, dus we kunnen in ieder geval achterhalen welke velden deze heeft en hoe ze worden genoemd. Gewapend met deze informatie kunnen we ons probleem oplossen:

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());
   }
}
Zoals vermeld in de opmerkingen, is alles met het nameveld eenvoudig, aangezien de klassenontwikkelaars een setter hebben geleverd. U weet al hoe u objecten kunt maken van standaardconstructors: we hebben newInstance()hiervoor. Maar we zullen wat moeten sleutelen aan het tweede veld. Laten we uitzoeken wat hier aan de hand is :)

Field age = clazz.getDeclaredField("age");
Hier, met behulp van ons Class clazzobject , hebben we toegang tot het ageveld via de getDeclaredField()methode. Hiermee kunnen we het leeftijdsveld als een Field ageobject krijgen. Maar dit is niet genoeg, omdat we niet zomaar waarden aan privatevelden kunnen toekennen. Om dit te doen, moeten we het veld toegankelijk maken met behulp van de setAccessible()methode:

age.setAccessible(true);
Zodra we dit aan een veld doen, kunnen we een waarde toekennen:

age.set(cat, 6);
Zoals je kunt zien, heeft ons Field ageobject een soort inside-out setter waaraan we een int-waarde doorgeven en het object waarvan het veld moet worden toegewezen. We voeren onze main()methode uit en zien:

Cat{name='Fluffy', age=6}
Uitstekend! We hebben het gedaan! :) Laten we eens kijken wat we nog meer kunnen doen...

Hoe een instantiemethode bij naam aan te roepen.

Laten we de situatie in het vorige voorbeeld iets veranderen. Laten we zeggen dat de Catklasse-ontwikkelaar geen fout heeft gemaakt met de getters en setters. Alles is in orde wat dat betreft. Nu is het probleem anders: er is een methode die we zeker nodig hebben, maar de ontwikkelaar heeft deze privé gemaakt:

private void sayMeow() {

   System.out.println("Meow!");
}
Dit betekent dat als we objecten in ons programma maken , we de methode er niet op Catkunnen aanroepen . sayMeow()Zullen we katten hebben die niet miauwen? Dat is vreemd :/ Hoe gaan we dit oplossen? Nogmaals, de Reflection API helpt ons! We kennen de naam van de methode die we nodig hebben. Al het andere is een technisch detail:

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();
   }
}
Hier doen we grotendeels hetzelfde als bij het betreden van een privéveld. Eerst krijgen we de methode die we nodig hebben. Het is ingekapseld in een Methodobject:

Method sayMeow = clazz.getDeclaredMethod("sayMeow");
Met de getDeclaredMethod()methode kunnen we naar privémethoden gaan. Vervolgens maken we de methode aanroepbaar:

sayMeow.setAccessible(true);
En tot slot noemen we de methode op het gewenste object:

sayMeow.invoke(cat);
Hier ziet onze methodeaanroep eruit als een "callback": we zijn gewend een punt te gebruiken om een ​​object naar de gewenste methode te wijzen ( cat.sayMeow()), maar als we met reflectie werken, geven we het object door aan de methode waarop we willen bellen die methode. Wat staat er op onze console?

Meow!
Alles werkte! :) Nu kun je de enorme mogelijkheden zien die het reflectiemechanisme van Java ons biedt. In moeilijke en onverwachte situaties (zoals onze voorbeelden met een klas uit een gesloten bibliotheek) kan het ons enorm helpen. Maar zoals bij elke grote macht brengt het een grote verantwoordelijkheid met zich mee. De nadelen van reflectie worden beschreven in een speciale sectie op de Oracle-website. Er zijn drie belangrijke nadelen:
  1. Prestaties zijn slechter. Methoden die via reflectie worden genoemd, presteren slechter dan methoden die op de normale manier worden aangeroepen.

  2. Er zijn beveiligingsbeperkingen. Met het reflectiemechanisme kunnen we het gedrag van een programma tijdens runtime veranderen. Maar op uw werkplek, wanneer u aan een echt project werkt, kunt u te maken krijgen met beperkingen die dit niet toestaan.

  3. Risico van blootstelling van interne informatie. Het is belangrijk om te begrijpen dat reflectie een directe schending is van het principe van inkapseling: het geeft ons toegang tot privévelden, methoden, enz. Ik denk niet dat ik hoef te vermelden dat een directe en flagrante schending van de principes van OOP moet worden gebruikt tot alleen in de meest extreme gevallen, wanneer er geen andere manieren zijn om een ​​probleem op te lossen om redenen buiten uw macht.

Gebruik reflectie verstandig en alleen in situaties waarin het niet kan worden vermeden, en vergeet de tekortkomingen ervan niet. Hiermee is onze les ten einde. Het is best lang geworden, maar je hebt veel geleerd vandaag :)
Opmerkingen
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION