Reflexia v Jave
Ešte než opustíme OOP sekciu, chcel by som vás zoznámiť s pokročilou technikou – reflexiou.
Reflexia
Reflexia je nástroj, pomocou ktorého je možné zistiť za behu programu informácie o nejakej triede alebo inštancii, pristúpiť k jej atribútom a v neposlednom rade meniť hodnoty týchto atribútov.
Na čo je to dobré?
Vďaka reflexii môžeme napr. vytvoriť inštanciu triedy, ktorej názov
nám niekde prišiel ako String a pred spustením programu teda
nevieme, akú inštanciu tu budeme vytvárať. To sa môže hodiť napr. pri
parsovaní nejakých textových súborov, o ktorých vopred nevieme, čo
obsahujú. Možno ste už niekedy použili bezduché vetvenia typu:
if (attribute == "first_name") { user.first_name = value; } else if (attribute == "last_name") { user.last_name = value; }
Reflexia nám v tejto situácii umožní siahnuť na atribút podľa jeho
názvu odovzdaného ako String a objektu, ktorému má patriť.
Reflexia je však ešte mocnejší nástroj, ktorý môže v nesprávnych
rukách narobiť veľkú škodu. Na konci dnešnej lekcie si ukážeme, ako je
pomocou nej možné modifikovať private atribúty zvonku. Ouch
Reflexia je tiež relatívne
náročná na pamäť, preto by ste túto techniku mali využívať iba
minimálne a v krajných prípadoch, kde naozaj dáva zmysel. Môžeme ju
použiť napr. pri už spomínanom parsovaní, nikto nemá rád kilometrové
konštrukcie switch 
class
Než sa pustíme do praxe, treba ešte prebrať trochu teórie. Najskôr sa
zoznámime s metódami, pomocou ktorých môžeme s triedami manipulovať. K
metódam pre reflexiu sa dostaneme cez triedu, ktorú chceme skúmať/upravovať
pomocou magického slova class, teda:
SomeClass.class
Každé múdre IDE nám dokáže napovedať, ktoré metódy sú k dispozícii. Tu je zoznam tých "najdôležitejších".
Základné informácie
Na získanie základných informácií o triede použijeme metódy:
getPackage()- Vráti package, v ktorom sa trieda nachádza.getName()- Vráti názov triedy.getConstructors()- Vráti pole všetkých konštruktorov.getConstructor(String name)- Vráti inštanciu konkrétneho konštruktora.getSuperclass()- Vráti typ predka.getAnnotations()- Vráti pole anotácií triedy.getModifiers()- Vráti číslo, ktoré obsahuje zakódovaný zoznam modifikátorov.
Atribúty a metódy
Na prácu s atribútmi a metódami danej triedy použijeme:
getFields()- Vráti pole atribútov triedy vrátane zdedených.getField(String name)- Vráti inštanciu jedného atribútu triedy.getMethods()- Vráti pole metód v testovanej triede vrátane zdedených.getMethod(String name)- Vráti inštanciu jednej metódy v testovanej triede.getDeclaredFields()- Vráti pole deklarovaných atribútov triedy.getDeclaredField(String name)- Vráti inštanciu jedného deklarovaného atribútu triedy.getDeclaredMethods()- Vráti pole deklarovaných metód v testovanej triede.getDeclaredMethod(String name)- Vráti inštanciu jednej metódy v testovanej triede.
isInterface()- Vráti hodnotutrue, ak ide o rozhranie, inak hodnotufalse.isEnum()- Vráti hodnotutrue, ak sa jedná oenum, inak hodnotufalse.
Ak použijeme gettery na declared pre získanie
konkrétnej metódy/atribútu a odkážeme sa pritom na metódu/atribút
predka, vyvolá sa výnimka NoSuchMethodException.
Metódy inštancií
Niektoré gettery nám vracajú inštanciu tried, napríklad metódy:
getConstructor(),getMethod(),getField()agetAnnotations().
Nad inštanciami týchto tried možno volať veľké množstvo rovnakých metód, ktoré sú uvedené vyššie. Pre konštruktory a metódy pribudli metódy na získanie ich parametrov, prípadne návratového typu:
getParameterTypes()- Vráti pole typov parametrov.getReturnType()- Vráti návratový dátový typ.
Modifikátory
Ďalej sme spomenuli metódu getModifiers(), ktorá vráti
záhadné číslo. Toto číslo nám pomôže dekódovať trieda
Modifier, ktorá nám ponúka statické metódy na zistenie
jednotlivých modifikátorov:
Modifier.isPublic(modifier)Modifier.isPrivate(modifier)Modifier.isProtected(modifier)Modifier.isStatic(modifier)Modifier.isFinal(modifier)Modifier.isSynchronized(modifier)Modifier.isVolatile(modifier)Modifier.isTransient(modifier)Modifier.isNative(modifier)Modifier.isAbstract(modifier)
Kocka
Dosť bolo teórie, ideme si vyššie spomínané metódy vyskúšať v
praxi. Spomínate si na jednoduchú hraciu kocku z jednej
lekcie kurzu? Dnes budeme všetko demonštrovať práve na triede
RollingDie. Jej kód je nasledujúci:
public class RollingDie { private Random random; private int sidesCount; public RollingDie() { sidesCount = 6; random = new Random(); } public RollingDie(int sidesCount) { this.sidesCount = sidesCount; random = new Random(); } public int getSidesCount() { return sidesCount; } public int roll() { return random.nextInt(sidesCount) + 1; } @Override public String toString() { return String.format("Rolling die with %s sides", sidesCount); } }
Získanie informácií o triede za behu programu
Pomocou reflexie si necháme vypísať podrobné informácie o tejto triede:
System.out.println("Class name: " + RollingDie.class.getName()); System.out.println("----- Class constructors ------"); final Constructor<?>[] constructors = RollingDie.class.getConstructors(); for (Constructor<?> constructor : constructors) { System.out.println("\t Name: " + constructor.getName()); System.out.println("\t\t Constructor parameters: " + Arrays.toString(constructor.getParameterTypes())); } System.out.println(); System.out.println("------- Class attributes --------"); final Field[] fields = RollingDie.class.getDeclaredFields(); for (Field field : fields) { System.out.println("\t Name: " + field.getName()); final int modifier = field.getModifiers(); System.out.println("\t\t Modifiers.isPublic: " + Modifier.isPublic(modifier)); System.out.println("\t\t Modifiers.isPrivate: " + Modifier.isPrivate(modifier)); System.out.println("\t\t Modifiers.isProtected: " + Modifier.isProtected(modifier)); System.out.println("\t\t Modifiers.isStatic: " + Modifier.isStatic(modifier)); System.out.println("\t\t Modifiers.isFinal: " + Modifier.isFinal(modifier)); System.out.println("\t\t Modifiers.isSynchronized: " + Modifier.isSynchronized(modifier)); System.out.println("\t\t Modifiers.isVolatile: " + Modifier.isVolatile(modifier)); System.out.println("\t\t Modifiers.isTransient: " + Modifier.isTransient(modifier)); System.out.println("\t\t Modifiers.isNative: " + Modifier.isNative(modifier)); System.out.println("\t\t Modifiers.isAbstract: " + Modifier.isAbstract(modifier)); } final Method[] methods = RollingDie.class.getDeclaredMethods(); System.out.println("Declared methods: " + Arrays.asList(methods)); System.out.println("Methods: " + Arrays.asList(RollingDie.class.getMethods())); for (Method method : methods) { System.out.println("\t Name: " + method.getName()); final int modifier = method.getModifiers(); System.out.println("\t\t Modifiers.isPublic: " + Modifier.isPublic(modifier)); System.out.println("\t\t Modifiers.isPrivate: " + Modifier.isPrivate(modifier)); System.out.println("\t\t Modifiers.isProtected: " + Modifier.isProtected(modifier)); System.out.println("\t\t Modifiers.isStatic: " + Modifier.isStatic(modifier)); System.out.println("\t\t Modifiers.isFinal: " + Modifier.isFinal(modifier)); System.out.println("\t\t Modifiers.isSynchronized: " + Modifier.isSynchronized(modifier)); System.out.println("\t\t Modifiers.isVolatile: " + Modifier.isVolatile(modifier)); System.out.println("\t\t Modifiers.isTransient: " + Modifier.isTransient(modifier)); System.out.println("\t\t Modifiers.isNative: " + Modifier.isNative(modifier)); System.out.println("\t\t Modifiers.isAbstract: " + Modifier.isAbstract(modifier)); System.out.println("\t\t Parameters: " + Arrays.toString(method.getParameterTypes())); System.out.println("\t\t Return type: " + method.getReturnType()); }
Nezabudneme na importy:
import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.Arrays;
Výsledok výpisu bude vyzerať takto:
Konzolová aplikácia
Class name: com.ictdemy.reflection.RollingDie
----- Class constructors ------
Name: com.ictdemy.reflection.RollingDie
Constructor parameters: []
Name: com.ictdemy.reflection.RollingDie
Constructor parameters: [int]
------- Class attributes --------
Name: random
Modifiers.isPublic: false
Modifiers.isPrivate: true
Modifiers.isProtected: false
Modifiers.isStatic: false
Modifiers.isFinal: false
Modifiers.isSynchronized: false
Modifiers.isVolatile: false
Modifiers.isTransient: false
Modifiers.isNative: false
Modifiers.isAbstract: false
Name: sidesCount
Modifiers.isPublic: false
Modifiers.isPrivate: true
Modifiers.isProtected: false
Modifiers.isStatic: false
Modifiers.isFinal: false
Modifiers.isSynchronized: false
Modifiers.isVolatile: false
Modifiers.isTransient: false
Modifiers.isNative: false
Modifiers.isAbstract: false
Declared methods: [
public java.lang.String com.ictdemy.reflection.RollingDie.toString(),
public int com.ictdemy.reflection.RollingDie.getSidesCount(),
public int com.ictdemy.reflection.RollingDie.roll()
]
Methods: [
public java.lang.String com.ictdemy.reflection.RollingDie.toString(),
public int com.ictdemy.reflection.RollingDie.getSidesCount(),
public int com.ictdemy.reflection.RollingDie.roll(),
public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException,
public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException,
public final void java.lang.Object.wait() throws java.lang.InterruptedException,
public boolean java.lang.Object.equals(java.lang.Object),
public native int java.lang.Object.hashCode(),
public final native java.lang.Class java.lang.Object.getClass(),
public final native void java.lang.Object.notify(),
public final native void java.lang.Object.notifyAll()
]
Name: toString
Modifiers.isPublic: true
Modifiers.isPrivate: false
Modifiers.isProtected: false
Modifiers.isStatic: false
Modifiers.isFinal: false
Modifiers.isSynchronized: false
Modifiers.isVolatile: false
Modifiers.isTransient: false
Modifiers.isNative: false
Modifiers.isAbstract: false
Parameters: []
Return type: class java.lang.String
Name: getSidesCount
Modifiers.isPublic: true
Modifiers.isPrivate: false
Modifiers.isProtected: false
Modifiers.isStatic: false
Modifiers.isFinal: false
Modifiers.isSynchronized: false
Modifiers.isVolatile: false
Modifiers.isTransient: false
Modifiers.isNative: false
Modifiers.isAbstract: false
Parameters: []
Return type: int
Name: roll
Modifiers.isPublic: true
Modifiers.isPrivate: false
Modifiers.isProtected: false
Modifiers.isStatic: false
Modifiers.isFinal: false
Modifiers.isSynchronized: false
Modifiers.isVolatile: false
Modifiers.isTransient: false
Modifiers.isNative: false
Modifiers.isAbstract: false
Parameters: []
Return type: int
Modifikácia atribútov
Nakoniec si ukážeme, prečo je reflexia taký mocný nástroj. Trieda
RollingDie si uchováva informáciu o počte stien v
privátnom atribúte sidesCount. Najprv si
vytvoríme inštanciu kocky. Z tej si pomocou reflexie vytiahneme jej atribút
sidesCount, resp. získame inštanciu, ktorá tento atribút
reprezentuje. Tej potom upravíme jej hodnotu na novú. Voilà, práve sme
prepísali hodnotu privátneho atribútu zvonku!
Pripravíme si nasledujúci kód:
RollingDie die = new RollingDie(); System.out.println("Original sides count: " + die.getSidesCount()); final java.lang.reflect.Field sidesCountField = RollingDie.class.getDeclaredField("sidesCount"); sidesCountField.set(die, 12); System.out.println("New sides count: " + die.getSidesCount());
Aby sme program spustili, musíme ešte upraviť hlavičku metódy
main() alebo použiť bloky try-catch. My tu len
narýchlo upravíme hlavičku:
public static void main(String[] args) throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
Keď kód teraz spustíme, aplikácia nám vynadá, že sa snažíme pristúpiť k privátnemu atribútu:
Konzolová aplikácia
Exception in thread "main" java.lang.IllegalAccessException: class com.ictdemy.reflection.Main cannot access a member of class com.ictdemy.reflection.RollingDie with modifiers "private"
To je úplne v poriadku. Za normálnych okolností nie je dobrý nápad upravovať privátne atribúty, nikdy nemôžete tušiť, akú škodu tým napáchate.
Keď sme si ale celkom istí, čo robíme, použijeme pred
setterom magickú metódu setAccessible(), pomocou ktorej
"sprístupníme" atribút pomocou reflexie. Od tejto chvíle bude možné
atribút upravovať, avšak iba v rámci reflexie nad konkrétnou inštanciou
triedy Field.
Aktualizovaný kód bude vyzerať nasledovne:
RollingDie die = new RollingDie(); System.out.println("Original sides count: " + die.getSidesCount()); final Field sidesCountField = RollingDie.class.getDeclaredField("sidesCount"); sidesCountField.setAccessible(true); sidesCountField.set(die, 12); System.out.println("New sides count: " + die.getSidesCount());
Teraz už bude všetko v poriadku a hodnota atribútu bude úspešne zmenená:
Konzolová aplikácia
Original sides count: 6
New sides count: 12
Touto cestou je možné dokonca zmeniť aj atribút
označený kľúčovým slovom final (konštantu)!
Výnimky pri práci s reflexiou
Ak iba získavame informácie o triede, tak žiadne výnimky riešiť
nemusíme. Ak ale začneme získavať konkrétne metódy/atribúty pomocou
metód getMethod(String name) alebo
getField(String name), budeme musieť ošetriť prípad, keď
metóda alebo atribút nebudú existovať. To isté platí pri prepisovaní
hodnôt atribútov. Výnimky sa preberajú v kurze Súbory v Jave.
