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.
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.
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:
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.
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ę).
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:
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.