
Cat
klasse 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 Animal
bovenliggende klasse. Eerder hebben we zelfs een klasse gemaakt die een dierenkliniek vertegenwoordigt, waaraan we een Animal
object (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 Cat
aan 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 CodeAnalyzer
klasse 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.
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:
- Identificeer/bepaal de klasse van een object.
- Krijg informatie over klassemodifiers, velden, methoden, constanten, constructors en superklassen.
- Zoek uit welke methodes bij een geïmplementeerde interface(s) horen.
- Maak een instantie van een klasse waarvan de klassenaam pas bekend is als het programma wordt uitgevoerd.
- Haal de waarde van een instantieveld op en stel deze in op naam.
- Roep een instantiemethode bij naam aan.
Hoe de klasse van een object te identificeren/bepalen
Laten we beginnen met de basis. Het toegangspunt tot de Java-reflectie-engine is deClass
klasse. Ja, het ziet er echt grappig uit, maar dat is wat reflectie is :) Met behulp van de Class
klasse 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 Cat
les bewust in een apart learn.codegym
pakket 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 eenvoudigsteAnimal
bovenliggende klasse:
package learn.codegym;
public class Animal {
private String name;
private int age;
}
En we laten onze Cat
klasse erven Animal
en 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: private
variabelen 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. OnzeCat
klas heeft er nog geen, dus laten we het toevoegen:
public Cat() {
}
Hier is de code voor het maken van een Cat
object 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 name
en age
worden weergegeven op de console omdat we code hebben geschreven om ze uit te voeren in de toString()
methode van de Cat
klasse. 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. 
newInstance()
:) , maken we een nieuw object van deze klasse. Het is een andere zaak als deCat
constructor 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 Class
objecten gemaakt.
Class[] catClassParams = {String.class, int.class};
Ze komen overeen met de parameters van onze constructor (die alleen parameters String
en 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 oudeCat
klasse zijn), niet genoeg heeft geslapen op de avond voordat het ontwerp werd afgerond, de getter en setter voor het age
veld heeft verwijderd. Nu is deze klas naar jou toe gekomen. Het voldoet aan al uw behoeften, aangezien u alleen Cat
objecten 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 deprivate
modifier, 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 Cat
klasse, 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 name
veld 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 clazz
object , hebben we toegang tot het age
veld via de getDeclaredField()
methode. Hiermee kunnen we het leeftijdsveld als een Field age
object krijgen. Maar dit is niet genoeg, omdat we niet zomaar waarden aan private
velden 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 age
object 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 deCat
klasse-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 Cat
kunnen 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 Method
object:
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:
-
Prestaties zijn slechter. Methoden die via reflectie worden genoemd, presteren slechter dan methoden die op de normale manier worden aangeroepen.
-
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.
-
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.
GO TO FULL VERSION