Jakieś refleksje?

Być może programiści Javy  słyszeli o czymś takim jak mechanizm refleksji. Kiedyś sam miałem problem z zrozumieniem tego pojęcia, gdyż wydawało mi się dziwne, że coś, co od dawna istnieje w języku jest powielane i niepotrzebnie utrudniane. Teraz wiem, jak bardzo się myliłem.

Najpierw zacznijmy od samego pojęcia i jego definicji:

Mechanizm refleksji – pojęcie z dziedziny informatyki oznaczające proces, dzięki któremu program komputerowy może być modyfikowany w trakcie działania w sposób zależny od własnego kodu oraz od zachowania w trakcie wykonania. Paradygmat programowania ściśle związany z mechanizmem refleksji to programowanie refleksyjne. (źródło: Wikipedia)

Tu co niektórzy być może zauważyli, że definicja nie mówi, że jest to coś wyłącznego dla Javy. I słusznie, rzeczywiście tak jest, ale ja skupię się tu na tym, na czym najlepiej się znam, by przekazać wam jak najwięcej użytecznych informacji 🙂

Z punktu widzenia mnie sprzed roku, refleksje służyły tylko i wyłącznie do wywoływania funkcji w dziwny, skomplikowany sposób. Patrząc jednak na definicję, rysuje nam się zupełnie inny obraz tego mechanizmu. Możliwość zmieniania programu przez samego siebie podczas działania jest całkiem ciekawa, prawda?

Tu dużo młodych programistów może powiedzieć: zaraz, przecież mamy już możliwość zmieniania programu w czasie działania – wystarczą instrukcje warunkowe! Ok, ale instrukcje warunkowe nie zmieniają programu, tylko jego przebieg. Więc, o co w tym chodzi? Zacznijmy od podstaw. Z dowolnej klasy w Javie możemy utworzyć obiekt ją reprezentujący, nie mylić z jej obiektem 🙂 Obiekt ten jest klasy Class. Wygląda to tak:

Class klasa = MojaKlasa.class;

Dostajemy wtedy obiekt reprezentujący naszą klasę. A co możemy z nim zrobić? Możemy na przykład uzyskać listę jego metod:

Method[] metody = klasa.getMethods();

Co ciekawe, możemy wywołać którąś metodę specjalną metodą invoke klasy Method:

metody[0].invoke(obj, new Object[] {new Param1(), new Param2()});

//gdzie obj to obiekt na rzecz którego wywołujemy metodę,

//ParamX to parametr X funkcji, którą wywołujemy.

Tutaj uwaga, mimo, że argumentami funkcji mogą być typy proste takie jak int, to i tak musimy go ubrać w klasę Integer. To, że my decydujemy na rzecz jakiego obiektu wykonana zostanie funkcja, daje nam duże możliwości. Obiekty, które implementują te same interfejsy lub dziedziczą z wspólnej podklasy co klasa, z której wyłuskaliśmy tą metodę mogą być tu wykorzystane. Wyobraźmy sobie, że różne klasy (A i B) implementują metodę start() inaczej, a my chcemy akurat wywołać tą wersje metody start() z klasy B w klasie A. Nie ma z tym żadnego problemu, o ile znamy mechanizmy refleksji 🙂

interface Start{

  void start();

}

class A implements Start{
  int i = 4;
  @override
  void start(){
    for(i; i >= 0; i--){
      System.out.println("A!");
    }
  }
}
class B implements Start{
  int i = 10;
  @override
  void start(){
    for(i; i >= 0; i--){
      System.out.println("B!");
    }
  }
}
class Main{
  public static void main(String[] args){
    Class b = B.class;
    Method start = b.getMethod("start", new Class[]{});
    start.invoke(new A(), new Object[]{});
  }
}

Wynikiem będzie oczywiście B! powtórzone 4 razy.
Została tu użyta metoda getMethod(), która przyjmuje 2 argumenty w postaci nazwy metody oraz listy jej argumentów, zapisanych w postaci tablicy typu Class, która reprezentuje typy poszczególnych argumentów. Ta metoda jednak może wydobyć tylko dostępne dla nas z normalnego punktu widzenia metody. Ciekawostką (często wykorzystywaną) jest to, że możemy wydobyć także prywatne metody klasy, za pomocą metody getDeclaredMethod(). Przyjmuje ona te same argumenty, jednak może też wyłuskać prywatne i chronione metody klasy. Jednak, by ominąć mechanizmy kontroli zasięgów Javy, musimy oznaczyć taką metodę jak możliwą do wywołania bez względu na jej modyfikatory. Robimy to w ten sposób:

metoda.setAccesible(true);

Tak, właśnie oszukaliśmy Javę i daliśmy sobie możliwość wywołania prywatnej metody, nie będąc obiektem, który ma ją w sobie 🙂 Z metodami możemy także wyczyniać takie rzeczy jak sprawdzanie jej listy argumentów oraz typu zwracanego (w przypadku typów parametryzowanych jest trochę ciężej, ale nadal nie jest to niemożliwe :P)
Kolejne rzeczy, które możemy wyodrębnić z klasy to pola. Tu możemy się posunąć nawet trochę dalej, modyfikując ich wartość w żyjącym obiekcie, nawet, gdy pole to jest prywatne 😀

class A{
  private String startingStr = "A!";

  void start(){
    System.out.println(startingStr);
  }
}
class Main{
  public static void main(String[] args){
    Class a = A.class;
    A obiektA = new A();
    Field pole = a.getField("startingStr");
    pole.setAccesible(true);
    pole.set(obiektA, "B!");
    obiektA.start();
  }
}

Tak samo możemy odczytywać wartości, tyle że używamy tu metody get(), która jako argument przyjmuje tylko referencję do obiektu. Kolejne rzeczy, które możemy wyciągać z klas to ich superklasy (klasy, z których dziedziczą), modyfikatory, interfejsy które implementują, adnotacje, które są stosowane przy parametrach funkcji, polach, metodach i samej klasie oraz konstruktory, z których możemy tworzyć kolejne instancje naszej klasy za pomocą metody newInstance(), która przyjmuje argumenty w zwykłej postaci, tzn. do initializacji obiektu za pomocą konstruktora takiego jak ten:

class A{

  A(String str, int i){
    for(i; i >= 0; i--){
      System.out.println(str);
    }
  }
}

…musimy użyć wywołania takiego jak to:

Constructor c = A.class.getConstructor(new Class[]{String.class, int.class});
A objA = c.newInstance("yey!", 5);

Nie wspomniałem jeszcze tylko o jednym – tablicach. Tu robi się naprawdę skomplikowanie. By uzyskać sam obiekt tablicy za pomocą refleksji należy posłużyć się klasą Array:

int[] liczby = Array.newInstance(int.class, 5); //daje 5-elementową tablicę intów
Array.set(liczby, 0, 123); //ustawianie wartości w tablicy
System.out.println(Array.get(liczby, 0)); //uzyskiwanie wartości z tablicy

To było łatwe i intuicyjne. A co, gdy będziemy chcieli uzyskać klasę tablicy?

Class liczbyKlasa = Class.forName("[I");
Class ciagiKlasa = Class.forName("[Ljava.lang.String;");

Czas na trochę wyjaśnień. [I to wewnętrzna reprezentacja interpretera Javy, która oznacza klasę tablicy typu int Analogicznie, [Ljava.lang.String; to klasa tablicy typu String. Zaraz, widzicie ten średnik na końcu? Tak, jest tam wstawiony specjalnie, to nie błąd. To oznacza po prostu tablicę danego typu. Ciekawostką jest to, że za pomocą tej metody nie można uzyskać klas typów prymitywnych, wpisując np. I czy int. Można to zrobić za pomocą zapisu int.class.

Przykłady wykorzystania tego mechanizmu? Dostanie się do rejestru Windowsa 😛 To rozwiązanie korzysta z klasy Preferences, a dokładniej z jej prywatnych metod. Jak to wygląda? Najpierw musimy się dostać do wnętrza kodu klasy i uzyskać obiekty reprezentujące jej funkcje:

private static Preferences userRoot = Preferences.userRoot();
private static Preferences systemRoot = Preferences.systemRoot();
private static Class<? extends Preferences> userClass = userRoot.getClass();
private static Method regOpenKey = null;
private static Method regCloseKey = null;
private static Method regQueryValueEx = null;
private static Method regEnumValue = null;
private static Method regQueryInfoKey = null;
private static Method regEnumKeyEx = null;
private static Method regCreateKeyEx = null;
private static Method regSetValueEx = null;
private static Method regDeleteKey = null;
private static Method regDeleteValue = null;

static {
  try {
    regOpenKey = userClass.getDeclaredMethod("WindowsRegOpenKey",
        new Class[] { int.class, byte[].class, int.class });
    regOpenKey.setAccessible(true);
    regCloseKey = userClass.getDeclaredMethod("WindowsRegCloseKey",
        new Class[] { int.class });
    regCloseKey.setAccessible(true);
    regQueryValueEx = userClass.getDeclaredMethod("WindowsRegQueryValueEx",
        new Class[] { int.class, byte[].class });
    regQueryValueEx.setAccessible(true);
    regEnumValue = userClass.getDeclaredMethod("WindowsRegEnumValue",
        new Class[] { int.class, int.class, int.class });
    regEnumValue.setAccessible(true);
    regQueryInfoKey = userClass.getDeclaredMethod("WindowsRegQueryInfoKey1",
        new Class[] { int.class });
    regQueryInfoKey.setAccessible(true);
    regEnumKeyEx = userClass.getDeclaredMethod(  
        "WindowsRegEnumKeyEx", new Class[] { int.class, int.class,  
            int.class });  
    regEnumKeyEx.setAccessible(true);
    regCreateKeyEx = userClass.getDeclaredMethod(  
        "WindowsRegCreateKeyEx", new Class[] { int.class,  
            byte[].class });  
    regCreateKeyEx.setAccessible(true);  
    regSetValueEx = userClass.getDeclaredMethod(  
        "WindowsRegSetValueEx", new Class[] { int.class,  
            byte[].class, byte[].class });  
    regSetValueEx.setAccessible(true); 
    regDeleteValue = userClass.getDeclaredMethod(  
        "WindowsRegDeleteValue", new Class[] { int.class,  
            byte[].class });  
    regDeleteValue.setAccessible(true); 
    regDeleteKey = userClass.getDeclaredMethod(  
        "WindowsRegDeleteKey", new Class[] { int.class,  
            byte[].class });  
    regDeleteKey.setAccessible(true); 
  }
  catch (Exception e) {
    e.printStackTrace();
  }
}

Co tu się dzieje? Nic strasznego 🙂 Najpierw bierzemy obiekt klasy Preferences, z którego tworzymy obiekt jego klasy, reprezentujący kod prawdziwego obiektu. Potem, z tego obiektu wyciągamy funkcje. To wszystko? No, prawie, teraz jeszcze trzeba jakoś te metody wywołać. Tu, żeby nie zawalać posta, dodam tylko jedną metodę wywołującą metodę prywatną klasy Preferences. Dla przykładu, niech będzie to metoda createKey():

public static int [] createKey(Preferences root, int hkey, String key)
  throws IllegalArgumentException, IllegalAccessException,
  InvocationTargetException 
{
  return  (int[]) regCreateKeyEx.invoke(root,
      new Object[] { new Integer(hkey), toCstr(key) });
}

Ta metoda tworzy klucz w rejestrze, korzystając z trzech argumentów: obiektu Preferences, na rzecz którego wywołujemy metodę, który reprezentuje główną gałąź rejestru (userRoot albo systemRoot), główny klucz rejestru (adres do niego), np.

public static final int HKEY_CURRENT_USER = 0x80000001;

oraz klucz, który ma być w nim stworzony, przykładowo:

String key = "SOFTWAREadamj57LChar"

I to wszystko! To nie było takie trudne, prawda? :> Kolejnym wyzwaniem może być dynamiczne ładowanie klas, ale to temat na kolejnego posta 🙂 Mam nadzieję, że rozjaśniłem wam pojęcie refleksji, gdybyście mieli jakiekolwiek pytania lub sugestie, zapraszam do napisania komentarza!