NOVINKA: Kurz kybernetickej bezpečnosti teraz už od 0 €. Staň sa žiadaným profesionálom. Zisti viac:
NOVINKA: Staň sa dátovým analytikom od 0 € a získaj istotu práce, lepší plat a nové kariérne možnosti. Viac informácií:

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 hodnotu true, ak ide o rozhranie, inak hodnotu false.
  • isEnum() - Vráti hodnotu true, ak sa jedná o enum, inak hodnotu false.

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() a
  • getAnnotations().

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.


 

Všetky články v sekcii
Objektovo orientované programovanie v Jave
Článok pre vás napísal Petr Štechmüller
Avatar
Užívateľské hodnotenie:
128 hlasov
Autor se věnuje primárně programování v Javě, ale nebojí se ani webových technologií.
Aktivity