JAVA - obiekty refleksyjne

Marek Wierzbicki

Większość współczesnych języków programowania posiada możliwość dynamicznego dołączania bibliotek do wykonywanego programu. W zależności od specyfikacji tworzonego projektu mogą one zawierać najróżniejsze składniki, które będą mogły być dodawane bądź podmieniane już po stworzeniu programu zarówno przez jego twórcę jak i przyszłego użytkownika. JAVA posiada w swoich bibliotekach podobny mechanizm, znany pod nazwą obiekty refleksyjne. Dotyczy on co prawda klas, lecz wynika to z ortodoksyjnej obiektowości JAVY, w której wszystko musi być klasą lub obiektem. Jednak zaletą, nie spotykaną chyba w żadnym innym języku jest możliwość pełnego rozpoznania nieznanej klasy i użycie jej w sposób znacznie bardziej uniwersalny w porównaniu na przykład z biblioteką DLL. W dzisiejszym artykule przedstawię zarys idei obiektów refleksyjnych, czyli sposobu korzystania z klas nie znanych w czasie tworzenia programu.

Pierwszym miłym zaskoczeniem jest to, że wszystkie klasy tworzone w JAVIE mogą być używane zarówno w sposób tradycyjny jak i refleksyjny. Dzięki temu nie musimy na etapie tworzenia (tak jak to się dzieje na przykład w przypadku bibliotek DLL) deklarować przyszłego sposobu użycia klasy. Nie ma znaczenia ani modyfikator zakresu dostępu do pól czy metod jak i do samej klasy. Oczywiście klasa nie może być prywatna, gdyż wtedy nie będzie mogła być skompilowana jako główna klasa pliku JAVA. Ale poza tym nie ma żadnych ograniczeń - metody i pola mogą być publiczne, chronione czy też prywatne - będą mogły być użyte za pomocą refleksji w dokładnie taki sam sposób. Na wydruku 1 przedstawiam taką przykładową klasę, którą w dalszej części artykułu będę używał jako klasę dostarczającą w sposób dynamiczny funkcji do prostego kalkulatora. Oczywiście w praktycznym użyciu plik z używaną klasą jest dla nas niedostępny w czasie tworzenia programu. Dopiero w chwili uruchomienia go możemy mieć do niego dostęp. Może się to wiązać z tym, że ta część aplikacji tworzona jest przez inny zespół programistyczny (lub kupowana jest oddzielnie), bądź traktujemy ją jak bibliotekę dynamiczną, to znaczy rozszerzamy możliwości programu bez modyfikacji go z użyciem właśnie tej biblioteki.

Wydruk 1. Klasa przeznaczona do użycia jako refleksja

class kalkulator_fn{

float add(float a, float b){
return a + b;
}

float mul(float a, float b){
return a * b;
}

float div(float a, float b){
return a / b;
}

void nop(float a, float b){
}

float sum3(float a, float b, float c){
return a + b + c;
}

float ret(float a){
return a;
}
}

W dalszej części artykułu zaprzezentuję poszeczególne etapy korzystania z klas z użyciem obiektów refleksyjnych. W celu uproszczenia przykładu zrezygnowałem ze skomplikowanego interfejsu użytkownika, co umożliwia łatwiejszą analizę sedna sprawy. Interfejs zawiera pola wprowadzania danych (osobne dla każdego argumentu), listę dostępnych funkcji i przycisk, który wymusza wykonanie danego działania. Wynik wyświetlany jest z użyciem etykiety wstawionej do apletu. Aby ułatwić zrozumienie wydruk całego programu (w tym konkretnym przykładzie jest to aplet) podzieliłem go na mniejsze fragmenty. Na wydruku 2 znajduje się szablon omawianego apletu to znaczy wszystkie deklaracje importu, deklaracje pól, które są wykorzystywane w więcej niż jednej metodzie oraz same metody, których ciało zostało zastąpione pojedynczym komentarzem. Elementy składowe prezentowanego szablonu, które mają bezpośredni związek z refleksjami bądź ich użyciem w tym przykładzie zostały wytłuszczone. W artykule zaprezentuję wyłącznie sposób użycia nieznanych metod nieznanej klasy. Ograniczam się wyłącznie do najtrudniejszej kwestii tego sposobu używania obiektów. Wykorzystanie pól i konstruktorów odbywa się w sposób bardzo podobny. Zainteresowanych proszę o przeanalizowanie tego artykułu wraz z opisem właściwych klas znajdujących się w pakiecie java.lang.reflect.

Wydruk 2. Szablon apletu stosującego refleksje

import java.awt.*;
import java.awt.event.*;
import java.applet.*;
import java.lang.reflect.*;

public class refl extends Applet{

public List lst_func = new List(10);
Button btn_run = new Button();
TextField[] textfield = new TextField[2];
Class c;
Object obj;
Method m[];
int sel_m[] = new int[100];
Label wynik;

public void init(){
// ciało metody init
}

void lst_func_itemSelected(ItemEvent e) {
// lst_func_itemSelected
}

void btn_run_actionPerformed(ActionEvent e) {
// ciało metody btn_run_actionPerformed
}
}

Pierwszym działaniem w procesie użycia obiektów refleksyjnych jest analiza klasy pod kątem zadeklarowanych pól, konstruktorów i metod, ich typów oraz liczby i typów argumentów w konstruktorach i metodach (oczywiście pod warunkiem, że parametry te istnieją). Zadanie to wykonałem w metodzie init apletu (wydruk 3). Skorzystałem tu z wiedzy na temat struktury apletu oraz momentu wywoływania niektórych predefiniowanych metod. Metoda init uruchamiana jest bezpośrednio po pełnej inicjacji apletu, zawsze tylko jeden raz w pełnym cyklu życia apletu (czyli od chwili załadowania strony z apletem do jej zamknięcia bądź przeładowania). Wydruk 3 podzieliłem na fragmenty, ponumerowane w komentarzach. Poszczególne fragmenty kodu mają następujące znaczenie:

  1. Utworzenie obiektu klasy Class, która nadzoruje używanie obiektu refleksyjnego. Argumentem metody forName, która odpowiada za utworzenie nadzorcy jest nazwa pliku zawierającego klasę, którę użyjemy w sposób refleksyjny. Nazwa pliku nie może zawierać rozszerzenia class. Plik powinien być dostępny dla prgramu (apletu) to znaczy musi znajdować się w miejscu skąd pobierany jest aplet (może na przykład być w katalogu głównym archiwum JAR zawierającego aplet). Jeśli plik ten znajduje się w podkatalogu, wtedy przed nazwą pliku musi znajdować się struktura katalogu w którym znajduje się ten plik. Znakiem zmiany katalogu jest w argumencie tej metody zawsze kropka (bez względu na rzeczywisty znak występujący w konkretnym systemie operacyjnym, w którym tworzymy aplet). W analizowanym pliku powinna znaleźć się klasa o nazwie zgodnej z nazwą pliku. Jeśli będzie tam więcej klas (jeśli kompilator na to pozwoli) wtedy użyta zostanie wyłącznie klasa gówna, o nazwie zgodnej z nazwą pliku.
  2. Utworzenie instancji obiektu refleksyjnego. Jakkolwiek jest to egzemplarz obiektu zbudowany na podstawie definicji klasy, którą używamy w sposób refleksyjny proste użycie tego obiektu jest niemożliwe. Wynika to z faktu, że typ obiektu podlega tak zwanej bezpiecznej (rozszerzającej) konwersji. Tak więc utworzony obiekt jest typu Object. Nie możemy się więć odwołać do żadnych elementów charakterystycznych dla używanego obiektu refleksyjnego, zwłaszcza że nie możemy wykorzystać definicji używanej klasy (wszak zakładamy, że aplet będzie miał dostęp do tego pliku dopiero w fazie wykonania programu).
  3. Utworzenie listy metod dostępnych w obiekcie refleksyjnym. Warto zauważyć, że listę metod dostarcza metoda należąca do nadzorcy obiektu refleksyjnego, a nie sam obiekt refleksyjny.
  4. Pętla przetwarzania metod dostępnych w analizowanym obiekcie. W pętli wykonywane są po kolei następujące działania:
  5. Ustawienie etykiety przycisku i procedury obsługującej naciskanie tego przycisku. Przycisk ten umożliwia wykonanie działania wybranego z listy (zakładam, że nazwa metody obiektu - kalkulatora testm jednaoznacznie identyfikuje działanie). Więcej na ten temat w części opisującej wydruk 5, czyli uruchamianie metod z obiektów refleksyjnych.
  6. Ustawienie procedury obsługi selekcji poszczególnych wierszy listy funkcji dostępnych w analizowanym obiekcie. Metoda ta wstawia (między innymi) nazwę metody (którą w danym momencie możemy wykonać) na przycisk uruchamiający tą metodę. Więcej na ten temat w części opisującej wydruk 4.
  7. Inicjacja obiektów służących wprowadzaniu liczb do kalkulatora i wyprowadzaniu wyniku
  8. Umieszczenie wszystkich obiektów ekranowych w aplecie.
  9. Fragment kodu zajmujący się obsługą błędów. Zastosowany tu sposób obsługi błędów jest bardzo prosty. W praktyce powinno się stosować bardziej zaawansowaną obsługę błędów.

Po zakończeniu działania metody init aplet jest niemal gotowy do działania, jako prosty kalkulator z funkcjami dostarczanymi dynamicznie z użyciem refleksji.

Wydruk 3. Inicjacja apletu z refleksjami (analiza nazw i liczby parametrów metod)

public void init(){
int i, j, k;
try {
   // 1
   c = Class.forName("testm");
   // 2
   obj = Class.forName("testm").newInstance();
   // 3
   m = c.getDeclaredMethods();
   // 4
   k = 0;
   for (i=0; i<m.length; i++){
       if (! m[i].getReturnType().toString().equals("void")){
          Class partypes[] = m[i].getParameterTypes();
          if (partypes.length>0 & partypes.length<3 & k<100){
             lst_func.add(m[i].getName());
             sel_m[k] = i;
             k++;
          }
       }
   }
   // 5
   btn_run.setLabel("");
   btn_run.addActionListener(new ActionListener(){
     public void actionPerformed(ActionEvent e) {
       btn_run_actionPerformed(e);
     }});
   // 6
   lst_func.addItemListener(new ItemListener() {
     public void itemStateChanged(ItemEvent e) {
       lst_func_itemSelected(e);
     }});
   // 7
   textfield[0] = new TextField("", 10);
   textfield[1] = new TextField("", 10);
   wynik = new Label("wynik");
   // 8
   this.add(lst_func, null);
   this.add(textfield[0], null);
   this.add(textfield[1], null);
   this.add(btn_run, null);
   this.add(wynik, null);
}
// 9
catch (Throwable e) {
   System.err.println(e);
}
}

Do pełnej gotowości apletu do pracy brakuje jeszcze pewnej interaktywności związanej ze zmianami wyboru metod, którą zamierzamy użyć (jak wcześniej napisałem metody te nazwami reprezentują funkcje kalkulatora). Wspomniana interaktywność apletu polega na tym, że w miarę zmiany wyboru pozycji z listy dostępnych metod nazwa tej metody umieszczana jest na przycisku uruchamiającym działanie skojarzone z tą nazwą. Ponadto w zależności od dostępnej liczby parametrów jedno z pól wprowadzania danych jest blokowane bądź odblokowywane. Jak wcześniej napisałem założyłem, że używane w kalkulatorze funkcje mogą mieć jeden bądź dwa argumenty liczbowe - w zależności od odczytanej liczby parametrów jedno z pól jest blokowane bądź odblokowywane. Metoda zaprezentowana na wydruku 4 jest wywoływana automatycznie przez obiekt nasłuchujący zdarzeń typu Item przez obiekt - listę dostępnych metod (na wydruku 3 punkt 6 dokonane jest ustawienie obsługi zdarzeń przez tą metodę).

Wydruk 4. Dopasowanie wyglądu do liczby parametrów

void lst_func_itemSelected(ItemEvent e){
   btn_run.setLabel(lst_func.getSelectedItem());
   int m_nr = sel_m[lst_func.getSelectedIndex()];
   Class partypes[] = m[m_nr].getParameterTypes();
   if (partypes.length==1) {
      textfield[1].setEditable(false);
   }else{
      textfield[1].setEditable(true);
   }
}

Na zakończenie pozostało mi pokazanie akcji wykonywanej po naciśnieciu przycisku z nazwą funkcji. Obsługą tego zdarzenia zajmuje się metoda zaprezentowana na wydruku 5 (wskazanie tej metody, jako obsługującej tą akcję zostało pokazane na wydruku 3 w punkcie 5). Metodę tą również podzieliłem na części, których znaczenie wyjaśnię w punktach:

  1. Konwersja położenia funkcji na liście, na numer obiektu w tablicy nadzorcy metod wypełnionego wcześniej.
  2. Utworzenie tablicy obiektów, nadzorców parametrów uruchamianej metody.
  3. Utworzenie tablicy obiektów reprezentujących parametry, które przekażemy do wywoływanej metody. Podobnie jak w przypadku samego obiektu dla każdego parametru musi istnieć zarówno nadzorca parametru jak i obiekt reprezentujący ten parametr.
  4. Pobranie parametrów z pól tekstowych i przekształcenie ich do postaci zgodnej z deklarowanym typem parametru. Warto zauważyć, że nie stosuję tu żadnej kontroli poprawności działania (poza globalnym wyłapywaniem błędów). W praktycznym użyciu ten fragment kodu powinien być obłożony dodatkowymi zabezpieczeniami (poza tymi, związanymi z samym procesem obsługi obiektów refleksyjnych).
  5. Dynamiczne wywołanie metody obiektu refleksyjnego. W wywołaniu tym korzystam z informacji odczytanych na temat metod bezpośrednio po uruchomieniu programu (i zapisanych w tablicy nadzorców metod oraz wskazaniu do obiektu refleksyjnego) oraz z parametrów odczytanych lokalnie. Wynik jeśli istnieje (w naszym przypadku sprawdziłem wcześniej, że wynik metody istnieje, czyli jest różny od void) jest zapisywany jako wskazanie do obiektu przechowującego wynik.
  6. Sprawdzenie (z użyciem nadzorcy metody) typu wyniku i konwersja a następnie wyświetlenie w aplecie.
  7. Szczątkowy fragment obsługi błędów. W praktyce obsługa błędów powinna być bardziej zaawansowana i obejmować zarówno tworzenie obiektów nadzorców, samych obiektów parametrów oraz odbiór danych z wierszy wprowadzania tekstu. Ponadto należałoby sprawdzić, czy wstępna inicjacja tablicy nadzorców metod i samego obiektu refleksyjenego z nadzorcą powiodła się.

Wydruk 5. Użycie metody z obiektu refleksyjnego

void btn_run_actionPerformed(ActionEvent e) {
   if (lst_func.getSelectedIndex()>-1){
      try {
          // 1
          int m_nr = sel_m[lst_func.getSelectedIndex()];
          // 2
          Class partypes[] = m[m_nr].getParameterTypes();
          // 3
          Object arglist[] = new Object[partypes.length];
          // 4
          for (int j=0; j<partypes.length; j++){
              if (partypes[j].toString().equals("integer")){
                 arglist[j] = new Integer(textfield[j].getText());
              }
              if (partypes[j].toString().equals("float")){
                 arglist[j] = new Float(textfield[j].getText());
              }
          }
          // 5
          Object retobj = m[m_nr].invoke(obj, arglist);
          // 6
          if (m[m_nr].getReturnType().toString().equals("integer")){
             Integer retval = (Integer)retobj;
             wynik.setText(retval.toString());
          }
          if (m[m_nr].getReturnType().toString().equals("float")){
             Float retval = (Float)retobj;
             wynik.setText(retval.toString());
          }
      // 7
      }
      catch (Throwable et) {
          System.err.println(et);
      }
   }
}

Jak widać korzystanie z obiektów refleksyjnych nie jest bardzo trudne. Jednak w porównaniu do tradycyjnego użycia obiektów należy spełnić wiele warunków zanim użyjemy którejś z metod. Należy sprawdzić liczbę i typy parametrów oraz wynik. Należy bardzo uważać na ewentualną możliwość przeciążenia metod oraz konstruktorów. Ważną kwestią, która może uniemożliwić korzystanie z obiektów refleksyjnych jest sposób tworzenia tych obiektów. Do tego celu nadzorca obiektu refleksyjnego wykorzystuje konstruktor bezparametrowy. Jeśli klasa wymaga tworzenia z użyciem innego konstruktora, to mogą pojawić się trudności z jej poprawnym użyciem. Kolejną kwestią jest obsługa błędów. Powinna ona być na znacznie wyższym poziomie, niż w przypadku zwykłych obiektów. Poza obsługą standardowych przypadków, które mogą zajść w czasie normalnej pracy powinna ona uwzględnić kwestie związane z dynamicznym dostępem do samego obiektu jak i jego składowych. Wszystkie te utrudnienia powodują, że obiekty refleksyjne powinny być używane tylko wtedy, gdy jest to naprawdę potrzebne. Dodatkowo jeśli planujemy wyposażyć nasz program w taką możliwość (to znaczy chcemy dać użytkownikom możliwość tworzenia zewnętrznych modułów, które będą przez nich używane pośrednio poprzez nasz program), wtedy najlepiej dośc dokładnie wyspecyfikować zawartość oraz zachowanie dostarczanych zewnętrznie klas (tak na przykład dzieje się w przypadku sterowników JDBC, które umożliwiają obsługę baz danych poprzez zewnętrzne sterowniki używane w trybie obiektów refleksyjnych).

Mimo wymienionych trudności warto jednak pamiętać, że obiekty refleksyjne dają nam większe możliwości niż zwykłe dynamicznie dołączane biblioteki stosowane w klasycznych językach programowania. Możliwości tem mogą znacząco uatrakcyjnić funkcjonalność naszego programu w JAVIE. Warto więc rozważyć ich użycie już na etapie tworzenia projektów programistycznych.