Cat
osztályt:
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 +
'}';
}
}
Mindent tud róla, és láthatja a benne rejlő területeket és módszereket. Tegyük fel, hogy hirtelen más állatosztályokat kell bevezetnie a programba. Valószínűleg létrehozhat egy osztály öröklődési struktúrát egy Animal
szülő osztállyal a kényelem kedvéért. Korábban még létrehoztunk egy állatorvosi rendelőt reprezentáló osztályt, aminek átadhattunk egy Animal
tárgyat (szülő osztály példányát), és a program megfelelően kezelte az állatot annak alapján, hogy kutyáról vagy macskáról van szó. Annak ellenére, hogy nem a legegyszerűbb feladatokról van szó, a program fordítási időben képes minden szükséges információt megtanulni az osztályokról. Ennek megfelelően, amikor átad egy Cat
tárgyat az állatorvosi klinika osztályának módszereihez amain()
módszerrel a program már tudja, hogy macska, nem kutya. Most képzeljük el, hogy más feladattal állunk szemben. Célunk egy kódelemző megírása. Létre kell hoznunk egy CodeAnalyzer
osztályt egyetlen metódussal: void analyzeObject(Object o)
. Ennek a módszernek:
- határozza meg a neki átadott objektum osztályát, és jelenítse meg az osztály nevét a konzolon;
- határozza meg az átadott osztály összes mezőjének nevét, beleértve a privátokat is, és jelenítse meg azokat a konzolon;
- határozza meg az átadott osztály összes metódusának nevét, beleértve a privátokat is, és jelenítse meg őket a konzolon.
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
}
}
Most már világosan láthatjuk, hogy ez a feladat miben tér el a korábban megoldott feladatoktól. Jelenlegi célunknál a nehézség abban rejlik, hogy sem mi, sem a program nem tudja, hogy pontosan mi kerül át aanalyzeClass()
módszer. Ha ilyen programot ír, más programozók elkezdik használni, és bármit átadhatnak ennek a metódusnak – bármely szabványos Java osztályt vagy bármely más osztályt, amit írnak. Az átadott osztálynak tetszőleges számú változója és metódusa lehet. Más szóval, nekünk (és a programunknak) fogalmunk sincs, milyen osztályokkal fogunk dolgozni. De ennek ellenére el kell végeznünk ezt a feladatot. És itt jön a segítségünkre a szabványos Java Reflection API. A Reflection API a nyelv hatékony eszköze. Az Oracle hivatalos dokumentációja azt javasolja, hogy ezt a mechanizmust csak tapasztalt programozók használják, akik tudják, mit csinálnak. Hamarosan meg fogod érteni, miért adunk ilyen előzetes figyelmeztetést :) Íme egy lista, hogy mit tehetsz a Reflection API-val:
- Határozza meg/határozza meg egy objektum osztályát.
- Információkat szerezhet az osztálymódosítókról, mezőkről, metódusokról, konstansokról, konstruktorokról és szuperosztályokról.
- Nézze meg, mely metódusok tartoznak egy megvalósított interfész(ek)hez.
- Hozzon létre egy példányt egy olyan osztályból, amelynek osztályneve a program végrehajtásáig nem ismert.
- Szerezze be és állítsa be egy példánymező értékét név szerint.
- Példánymetódus hívása név szerint.
Hogyan lehet azonosítani/meghatározni egy objektum osztályát
Kezdjük az alapokkal. A Java tükrözési motor belépési pontja azClass
osztály. Igen, nagyon viccesen néz ki, de ez a reflexió :) Az Class
osztály segítségével először meghatározzuk a metódusunknak átadott objektum osztályát. Próbáljuk meg ezt:
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));
}
}
Konzol kimenet:
class learn.codegym.Cat
Két dologra figyelj. Cat
Először is szándékosan külön csomagba helyeztük az osztályt learn.codegym
. Most láthatja, hogy a getClass()
metódus az osztály teljes nevét adja vissza. Másodszor elneveztük a változónkat clazz
. Ez egy kicsit furcsán néz ki. Ésszerű lenne "osztálynak" nevezni, de az "osztály" fenntartott szó a Java nyelvben. A fordító nem engedi, hogy a változókat így hívják. Ezt valahogy meg kellett kerülnünk :) Kezdésnek nem rossz! Mi volt még a képességek listáján?
Hogyan szerezhet információt osztálymódosítókról, mezőkről, metódusokról, konstansokról, konstruktorokról és szuperosztályokról.
Most a dolgok egyre érdekesebbek! A jelenlegi osztályban nincs konstansunk vagy szülőosztályunk. Adjuk hozzá őket, hogy teljes képet kapjunk. Hozd létre a legegyszerűbbAnimal
szülőosztályt:
package learn.codegym;
public class Animal {
private String name;
private int age;
}
Cat
És az osztályunkat örököljük Animal
, és hozzáadunk egy állandót:
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
}
Most megvan a teljes kép! Lássuk, mire képes a tükrözés :)
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));
}
}
Íme, amit a konzolon látunk:
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)]
Nézze meg mindazt a részletes osztályinformációt, amelyet meg tudtunk szerezni! És nem csak nyilvános információk, hanem privát információk is! Jegyzet: private
változók is megjelennek a listában. Az osztály "elemzése" lényegében befejezettnek tekinthető: a analyzeObject()
módszerrel mindent megtanulunk, amit csak lehet. De ez nem minden, amit a reflexióval tehetünk. Nem korlátozódunk az egyszerű megfigyelésre – továbblépünk a cselekvésre! :)
Hogyan hozzunk létre egy példányt egy olyan osztályból, amelynek osztályneve nem ismert, amíg a program le nem fut.
Kezdjük az alapértelmezett konstruktorral. A miCat
osztályunkban még nincs ilyen, úgyhogy tegyük hozzá:
public Cat() {
}
Íme a kód egy Cat
objektum tükrözéssel történő létrehozásához ( createCat()
metódus):
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());
}
}
Konzol bemenet:
learn.codegym.Cat
Konzol kimenet:
Cat{name='null', age=0}
Ez nem hiba: a name
és értékei age
azért jelennek meg a konzolon, mert toString()
az Cat
osztály metódusában kódot írtunk, hogy kiadjuk őket. Itt egy osztály nevét olvashatjuk, amelynek objektumát a konzolból létrehozzuk. A program felismeri annak az osztálynak a nevét, amelynek objektumát létrehozni kívánja. A rövidség kedvéért kihagytuk a megfelelő kivételkezelő kódot, amely több helyet foglalna el, mint maga a példa. Valós programban természetesen kezelni kell a helytelenül beírt neveket stb. tartalmazó helyzeteket. Az alapértelmezett konstruktor meglehetősen egyszerű, így amint látja, könnyen használható az osztály példányának létrehozására :) A metódus newInstance()
használata , létrehozunk egy új objektumot ebből az osztályból. Más kérdés, ha aCat
A konstruktor argumentumokat vesz fel bemenetként. Távolítsuk el az osztály alapértelmezett konstruktorát, és próbáljuk meg újra futtatni a kódunkat.
null
java.lang.InstantiationException: learn.codegym.Cat
at java.lang.Class.newInstance(Class.java:427)
Valami elromlott! Hibaüzenetet kaptunk, mert meghívtunk egy metódust egy objektum létrehozására az alapértelmezett konstruktor használatával. De most nincs ilyen kivitelezőnk. Tehát amikor a newInstance()
metódus fut, a tükrözési mechanizmus a régi konstruktorunkat használja két paraméterrel:
public Cat(String name, int age) {
this.name = name;
this.age = age;
}
De nem csináltunk semmit a paraméterekkel, mintha teljesen megfeledkeztünk volna róluk! A reflexió használata az argumentumok konstruktornak való átadásához egy kis "kreativitást" igényel:
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());
}
}
Konzol kimenet:
Cat{name='Fluffy', age=6}
Nézzük meg közelebbről, mi történik a programunkban. Objektumtömböt hoztunk létre Class
.
Class[] catClassParams = {String.class, int.class};
Megfelelnek a konstruktorunk paramétereinek (amelynek csak String
és int
paraméterei vannak). Átadjuk őket a clazz.getConstructor()
metódusnak, és hozzáférünk a kívánt konstruktorhoz. Ezek után már csak a metódust kell meghívnunk newInstance()
a szükséges argumentumokkal, és ne felejtsük el kifejezetten a kívánt típusba önteni az objektumot: Cat
.
cat = (Cat) clazz.getConstructor(catClassParams).newInstance("Fluffy", 6);
Most az objektumunk sikeresen létrejött! Konzol kimenet:
Cat{name='Fluffy', age=6}
Egyenesen haladunk :)
Hogyan lehet egy példánymező értékét név szerint lekérni és beállítani.
Képzelje el, hogy egy másik programozó által írt osztályt használ. Ezenkívül nem tudja szerkeszteni. Például egy JAR-ba csomagolt kész osztálykönyvtár. Az osztályok kódját elolvashatod, de nem tudod megváltoztatni. Tegyük fel, hogy a programozó, aki létrehozta az egyik osztályt ebben a könyvtárban (legyen ez a mi régiCat
osztályunk), mivel nem aludt eleget a terv véglegesítése előtti éjszakán, eltávolította a gettert és a settert a terepre age
. Ez az óra most eljött hozzád. Minden igényét kielégíti, mivel csak Cat
objektumokra van szüksége a programban. De szükség van rájuk, hogy legyen age
terük! Ez egy probléma: nem tudjuk elérni a pályát, mert megvan aprivate
módosítót, a gettert és a settert pedig az osztályt létrehozó alváshiányos fejlesztő törölte :/ Nos, a reflektálás segíthet ebben a helyzetben! Hozzáférünk az Cat
osztály kódjához, így legalább megtudhatjuk, hogy milyen mezői vannak és mi a neve. Ezen információk birtokában meg tudjuk oldani a problémánkat:
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());
}
}
Ahogy a megjegyzésekben is elhangzott, a name
mezőnyben minden egyértelmű, mivel az osztályfejlesztők biztosítottak egy settert. Már tudja, hogyan lehet objektumokat létrehozni alapértelmezett konstruktorokból: newInstance()
ehhez megvan a megfelelő. De trükköznünk kell a második mezőn. Találjuk ki, mi folyik itt :)
Field age = clazz.getDeclaredField("age");
Itt az objektumunk segítségével a metóduson keresztül Class clazz
érjük el a mezőt . Lehetővé teszi, hogy tárgyként megkapjuk a kormezőt . De ez nem elég, mert nem tudunk egyszerűen értékeket rendelni a mezőkhöz. Ehhez a következő módszerrel elérhetővé kell tennünk a mezőt : age
getDeclaredField()
Field age
private
setAccessible()
age.setAccessible(true);
Miután ezt megtettük egy mezőhöz, hozzárendelhetünk egy értéket:
age.set(cat, 6);
Amint láthatja, az objektumunknak Field age
van egy belső-ki beállítója, amelyhez átadunk egy int értéket és azt az objektumot, amelynek mezőjét hozzá kell rendelni. Futtatjuk main()
a módszerünket, és látjuk:
Cat{name='Fluffy', age=6}
Kiváló! Megcsináltuk! :) Lássuk, mit tehetünk még...
Hogyan hívjunk meg egy példánymetódus névvel.
Változtassunk kicsit az előző példában szereplő helyzeten. Tegyük fel, hogy azCat
osztályfejlesztő nem hibázott a getterekkel és a setterekkel. Ebből a szempontból minden rendben van. Most más a probléma: van egy módszer, amire feltétlenül szükségünk van, de a fejlesztő priváttá tette:
private void sayMeow() {
System.out.println("Meow!");
}
Ez azt jelenti, hogy ha objektumokat hozunk létre Cat
a programunkban, akkor nem tudjuk meghívni sayMeow()
rajtuk a metódust. Lesznek macskáink, akik nem nyávognak? Ez furcsa :/ Hogyan javítanánk? Ismét segítségünkre van a Reflection API! Ismerjük a szükséges módszer nevét. Minden más technikai kérdés:
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();
}
}
Itt nagyjából ugyanazt csináljuk, mint egy privát mező elérésekor. Először is megkapjuk a szükséges módszert. Egy objektumba van zárva Method
:
Method sayMeow = clazz.getDeclaredMethod("sayMeow");
A getDeclaredMethod()
módszer segítségével eljuthatunk a privát módszerekhez. Ezután hívhatóvá tesszük a metódust:
sayMeow.setAccessible(true);
És végül meghívjuk a metódust a kívánt objektumon:
sayMeow.invoke(cat);
Itt a metódushívásunk úgy néz ki, mint egy "visszahívás": megszoktuk, hogy egy pontot használunk, hogy egy objektumot a kívánt metódusra mutassunk ( cat.sayMeow()
), de amikor tükrözéssel dolgozunk, átadjuk a metódusnak azt az objektumot, amelyet meg akarunk hívni. azt a módszert. Mi van a konzolunkon?
Meow!
Minden működött! :) Most már láthatja, hogy a Java tükrözési mechanizmusa milyen hatalmas lehetőségeket kínál számunkra. Nehéz és váratlan helyzetekben (például egy zárt könyvtárból származó órával kapcsolatos példáink) valóban sokat segíthet. De mint minden nagy hatalom, ez is nagy felelősséggel jár. A reflexió hátrányait az Oracle weboldalának külön fejezete írja le . Három fő hátránya van:
-
A teljesítmény rosszabb. A reflexióval nevezett módszerek rosszabb teljesítményt nyújtanak, mint a normál módon.
-
Vannak biztonsági korlátozások. A tükrözési mechanizmus segítségével megváltoztathatjuk a program viselkedését futás közben. De a munkahelyén, amikor egy valós projekten dolgozik, olyan korlátokkal szembesülhet, amelyek ezt nem teszik lehetővé.
-
Belső információk expozíciójának kockázata. Fontos megérteni, hogy a reflexió a beágyazás elvének közvetlen megsértését jelenti: hozzáférést tesz lehetővé privát mezőkhöz, módszerekhez stb. Azt hiszem, nem kell megemlítenem, hogy az OOP elveinek közvetlen és kirívó megsértéséhez kell folyamodni. csak a legszélsőségesebb esetekben, amikor Önön kívül eső okok miatt nincs más megoldás a probléma megoldására.
GO TO FULL VERSION