2. Dynamiczna Java i programowanie komponentowe (JavaBeans)
2.1. Dynamiczne ładowanie klas.
W klasie Object metoda getClass. Zastosowana wobec dowolnego obiektu
zwraca odnośnik do jego klasy, do obiektu klasy java.lang.Class.
Obiekty klasy Class są klasami (to ważne: tu same klasy są obiektami)
Możemy wobec nich stosować różne metody klasy Class z pakietu java.lang, np.
getSuperClass(),
getInterfaces(),
newInstance(),
Obiektów-klas nie możemy tworzyć za pomocą konstrukcji new.
Jedynie poprzez użycie odpowiednich metod uzyskujemy odnośniki do tych obiektów.
Jedną z takich metod jest statyczna metoda klasy Class:
forName(String NazwaKlasy);
Np. pisząc:
Class c = Class.forName("javax.swing.JButton");
lub
Class c = Class.forName(java.awt.Button.class);
uzyskujemy deskryptor klasy przycisków i możemy się nim posłużyć przy tworzeniu obiektu:
JButton b = c.newInstance();
W statycznym przypadku, gdy wszystko jest ustalone "w źródle" programu, sens
takich konstrukcji jest niewielki. Ale bardzo często warto odłożyć pewne
ustalenia do fazy wykonania programu, zwiększając jego elastyczność i uniwersalność.
Wtedy dynamiczna reprezentacja obiektów-klas bardzo się przydaje.
Najprostszy przykład: odroczenie ustalenia sposobu obsługi akcji na przycisku
do fazy wykonania programu pokazano na poniższym wydruku. Jako argument
podajemy nazwę klasy, której obiekt obsługuje akcję. Możemy mieć wiele takich
(wariantowych) klas i bez żadnej rekompilacji zmieniać sposoby obsługi kliknięcia
w przycisk.
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import java.util.*;
class Main extends JFrame {
static void exit(String s) { System.out.println(s); System.exit(1); }
public static void main(String args[]) {
new Main(args[0]);
}
Main(String actionClassName) {
Class actionClass = null;
Action act = null;
try {
actionClass = Class.forName(actionClassName);
act = (Action) actionClass.newInstance();
} catch (Exception exc) {exit("Obiekt klasy akcji nie może być utworzony");}
JButton b = new JButton();
b.setAction(act);
getContentPane().add(b);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
pack();
show();
}
}
Uwaga:newInstance zwraca referencję do obiektu klasy Object - stąd potrzeba konwersji do typu Action.
Jeśli teraz przygotujemy dwie klasy, opisujące różne akcje np.
import javax.swing.*;
import java.awt.event.*;
public class DialogAction extends AbstractAction {
final static String ACTION_NAME = "Show msg";
public DialogAction() {
super(ACTION_NAME);
}
public void actionPerformed(ActionEvent e) {
JOptionPane.showMessageDialog(null, ACTION_NAME);
}
}
i
import javax.swing.*;
import java.awt.event.*;
public class PrintAction extends AbstractAction {
final static String ACTION_NAME = "Print";
public PrintAction() {
super(ACTION_NAME);
}
public void actionPerformed(ActionEvent e) {
System.out.println("Wykonan akcja: " + ACTION_NAME);
}
}
to uruchomienie głównego programu z argumentem: DialogAction:
java Main DialogAction
spowoduje, że przycisk w oknie uzysaka nazwę Show Msg, a jego klikniecie otworzy okienko komunikaty.
Natomiast po uruchomieniu programu z argmentem PrintAction
java Main PrintAction
nada przyciskowi nazwę Print, a jego kliknięcie wyprowadzi komunikat na konsolę.
Takie możliwości istniały w Javie od zawsze. Ale samo dynamiczne ładowanie
klas to zdecydowanie za mało, by można było tworzyć naprawdę elastyczne programy.
Dopiero w wersji 1.1 Java zyskała prawdziwą elastyczność dzięki wprowadzeniu
mechanizmów refleksji
2.2. Refleksja
Podstawowy programistyczny interfejs refleksji (Core Reflection API) realizowany jest przez klasy pakietu java.lang.reflect oraz rozbudowaną klasę Class z pakietu java.lang.
Refleksja oznacza możliwoć wykonywania W TRAKCIE WYKONANIA PROGRAMU następujących działań:
- uzyskiwania pełnej informacji o charakterystykach klasy (pola, metody, ich charakterystyki)
- działań na polach danego obiektu, poprzez ich nazwy,
- aktywowanie metod na rzecz danego obiektu poprzez ich nazwy i z podaniem argumentów.
Użycie refleksji pozwala m.in. na:
- stwierdzenie jakie i z jakimi argumentami metody występują w danej
klasie (np. podawanej dynamicznie w trakcie wykonania programu)
- dynamiczne wywoływanie metod (specyfikowanych w trakcie wykonania programu)
na rzecz jakiego obiektu (też dynamicznie ustalanego),
- dynamiczne uzyskiwanie i modyfikacje wartości pól obiektu.
Uzywając mechanizmów refleksji do metod i pól odwołujemy się poprzez ich nazwy, a nie identyfikatory.
Na czym polega różnica wobec statycznego przypadku?
W statyce odwołania są skonkretyzowane na etapie kompilacji.
Piszemy np. b.geText(). I tak już zostanie na zawsze.
W dynamice konstrukcja jest całkiem inna - właśnie posługująca się nazwą metody. Piszemy raczej tak: b.wykonaj("getText").
Klasy pakietu java.lang.reflect
Klasa | Przeznaczenie |
Array | Tworzenie tablic, uzyskiwanie i ustalanie wartości elementów |
Constructor | Informacja i dostęp do danego konstru
ktora danej klasy. W szczególności wykorzystanie dla tworzenia obiektu. |
Field | Informacja i dostęp do pola obiektu. Pobranie i zmiana wartości pola. |
Method | Informacja o danej metodzie danej klasy. Dynamiczne wywołanie metody na rzecz danego obiektu.
|
Modifier | Uzyskiwanie informacji o modyfikatorach składowej obiektu lub klasy. |
Użyteczne metody klasy java.lang.Class
Metoda | Przeznaczenie |
getClasses() getDeclaredClasses() | Zwraca tablicę obiektów klasy Class, które są składowymi danej klasy. |
getConstructors() getDeclaredConstructors() | Zwraca tablicę obiektów klasy Constructor; są to konstruktory danej klasy |
getConstructor(Class[]) getDeclaredConstructor(Class[]) | Zwraca obiekt konstruktor (obiekt klasy konstruktor), który ma podane typy argumentów |
getMethods() getDeclaredMethods() | Zwraca tablicę, zawierającą odnośniki do metod klasy. Metody są obiektami klasy Method. |
getMethod(String, Class[]) getDeclaredMethod(String, Class[]) | Zwraca metodę o podanej nazwie i podanych argumentach jako obiekt klsy Method. |
Uwaga : Rozróżnienie pomiędzy metodami mającymi i nie mającymi w nazwie tekstu "Declared" jest następujące:
-
metody bez "Declared" zwracają składowe tylko publiczne, ale jednocześnie również dziedziczone,
-
metody z "Declared" zwracają wszystkie składowe (również prywatne i zabezpieczone), ale bez dziedziczonych
2.3. Przykład wykorzystania refleksji
Napiszmy program, który daje użytkownikowi wybór co do następstw przyciśnięcia
jakiego przycisku. Co więcej, wyborów takich użytkownik może dokonywać w
fazie wykonania programu.
Zestaw możliwych akcji (na przyciskach) będzie zawarty w klasie ActionsSet np.
class ActionsSet {
public void Action1() { System.out.println("Action1"); }
public void Action2() { System.out.println("Action2"); }
public void Action3() { System.out.println("Action3"); }
public void Action4() { System.out.println("Action4"); }
public void Action5() { System.out.println("Action5"); }
}
Opcje dla użytkownika będą przedstawione w menu kontekstowym, otwieranym
na przycisku. Z tego menu może on wybrać (wielokrotnie i różnie w trakcie
działania programu) co konkretnie ma się stać, jeśli przyciśnie ten przycisk.
Poniższy wydruk pokazuje konstrukcję programu.
import java.lang.reflect.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
class Main extends JFrame implements ActionListener, MouseListener {
static void exit(String s) { System.out.println(s); System.exit(1); }
Method currAction = null; // bieżąca metoda obsługi
Class actionClass = null; // klasa obsługi
Object actionObject = null; // obiekt obsługi
JPopupMenu popUp = null; // menu kontekstowe z wyborem obsługi
Main() {
super("Test refleksji");
try {
actionClass = Class.forName("ActionsSet");
actionObject = actionClass.newInstance();
} catch(Exception exc) {
exit("Wadliwa klasa obsługi");
}
JButton b = new JButton("Akcja");
b.setFont(new Font("Dialog", Font.BOLD, 24));
b.addActionListener(this);
b.addMouseListener(this);
getContentPane().setLayout(new FlowLayout());
getContentPane().add(b);
popUp = new JPopupMenu();
createMenuItems();
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
pack();
show();
}
void createMenuItems() {
Method m[] = null;
try {
m = actionClass.getDeclaredMethods();
} catch(Exception exc) {
exit("Niedostępna info o metodach klasy obsługi");
}
for (int i = 0; i < m.length; i++) {
String name = m[i].getName();
JMenuItem mi = new JMenuItem(name);
mi.addActionListener(this);
popUp.add(mi);
}
}
void setCurrentAction(String action) {
Class args[] = {};
try {
currAction = actionClass.getMethod(action, args);
} catch(Exception exc) { exit("Nieznana metoda obsługi"); }
}
public void actionPerformed(ActionEvent e) {
Object src = e.getSource();
if (src instanceof JMenuItem)
setCurrentAction(((JMenuItem) src).getText());
else {
try {
Object args[] = { };
currAction.invoke(actionObject, args); // wywołanie metody obsługi
} catch(Exception exc) {
JOptionPane.showMessageDialog(null, "Akcja na przycisku nieustalona!");
}
}
}
public void mousePressed(MouseEvent e) {
showPopup(e);
}
public void mouseReleased(MouseEvent e) {
showPopup(e);
}
public void mouseEntered(MouseEvent e) {}
public void mouseExited(MouseEvent e) {}
public void mouseClicked(MouseEvent e) {}
private void showPopup(MouseEvent e) {
if (e.isPopupTrigger()) popUp.show(e.getComponent(), e.getX(), e.getY());
}
public static void main(String args[]) { new Main(); }
}
W programie korzystamy z klasy Method. Uzyskując odnośnik do konkretnej metody,
możemu ustalić ją jako obsługującą zdarzenie (metoda setCurrentAction). Przy
zajściu zdarzenia możemy "pośrednio" (dynamicznie) wywołać jego obsługę.
Służy temu metoda invoke z klasy Method, użyta w actionPerformed.
Działanie programu ilustruje rysunek.
Po wyborze opcji Action3 - kliknięcia w przycisk będą wyprowadzać na konsolę napisy "Akcja3".
Dwa komentarze
Kwestia argumentów: gdy określają sygnaturę metody (w getMethod) występują jako typy danych, a więc obiekty klasy Class.
Jako konkretne argumenty wywołania metody są konkretnymi obiektami, a więc
egzemplarzami klasy Object (obiekty klasy Object - w metodzie invoke).
W tym programie metoda wywoływana pośrednio nie miała argumentów.
Sposób tworzenia menu kontekstowego. prezentuje ono dostępne akcje,
a te - całkowicie - są opisywane poprzez klasę ActionsSet. Stąd ułatwienie
: dynamiczne pobranie akcji za pomocą metody getDeclaredMethods z klasy Class.
Nawet wtedy, gdy wszystko jest umieszczone w jednym pliku i nie przewiduje
się zmian w alternatywnych sposobach obsługi - to podejcie do programowania
zasługuje na uwagę.
2.4. Pojęcie JavaBean
JavaBean - (bean – ziarno) – to programowy komponent "wielokretnego
użytku", którego właściwości i funkcjonalność mogą być odczytywane i/lub
zmieniane uniwersalnymi środkami programistycznymi.
Uwaga: JavaBean oznacza zarówno obiekt, jak i klasę tego obiektu. Rozróżnienie
zawsze jasno wynika z kontekstu. O JavaBean będziemy mówić krócej "ziarno".
Co oznacza powyższa definicja?
Gdy wbudowujemy zewnętrzny komponent-ziarno do naszego programu, mamy do
dyspozycji środki programistyczne, które pozwalają uzyskać o nim informacje:
- jakie ma właściwości? jakie metody służą do ich pobierania i ustalania?
- jakie obsługuje zdarzenia? jakie zdarzenia mogą się mu przytrafiać?
- jakie metody udostępnia otoczeniu (eksportuje)?
Uniwersalność sposobów odczytywania i/lub zmieniana charakterystyk obiektu-ziarna opiera się na:
- uzgodnionym protokole. dotyczącym informacji o ziarnie (standardowe wzorce deklaracji metod i/lub klasy informacyjne)
- standardowych środkach pobierania informacji (introspekcja - realizowana przez klasę Introspector z pakietu java.beans)
- standardowych środkach dostosowania obiektu
2.5. Właściwości i akcesory
Ziarna mają właściwości (atrybuty).
Dostęp do własciwości zapewniają metody klasy-ziarna nazywane akcesorami..
Akcesor pobierający właściwości nazywa się getter, a ustalający – setter.
Wyróżniamy własciwości proste (w tym binarne) oraz właściwości indeksowane.
Właściwości proste mają jedną wartość, właściwości indeksowane – wiele wartości, przedstawianych jako tablica.
Standardowe wzorce deklaracji akcesorów są następujące
Dla prostej (niebinarnej) własciwości o nazwie NNN i typie Typ
getter: Typ getNNN()
setter: void setNNN(Typ)
np. dla ziarna javax.swing.JButton i własciwości background mamy
getter: Color getBackground()
i setter: void setBackground(Color).
Dla właściwości binarnej o nazwie NNN:
getter: boolean isNNN()
setter: void setNNN(boolean)
np. boolean isVisible(), setVisible(boolean)
Dla właściwości indeksowanej o nazwie NNN, której wartości reprezentowane są jako tablice elementów typu Typ:
getter elementu: Typ getNNN(int) // zwraca wartość podanego indeksu właściwości
setter elementu: vois setNNN(int, Typ) // ustala wartość podanego indeksu właściwości
getter tablicy: Typ[] getNNN()
setter tablicy: void setNNN(Typ[])
Właściwości ziarna mogą być związane (bounded).
O zmianie związanej właściwości ziarna mogą być zawiadamiane inne komponenty i reagować na tę zmianę.
Właściwości ziarna mogą być ograniczane (constrained).
Ograniczana właściwość – to taka, o której zmianie powiadamiane są zainteresowane
inne komponenty i są pytane o zgodę na tę zmianę. Jeśli którykolwiek z komponentów
nie da takiej zgody (zawetuje zmianę) – zmiana nie dochodzi do skutku.
Setter związanej i/lub ograniczanej właściwości ma obowiązek wygenerować zdarzenie klasy PropertyChangeEvent..
Klasy-ziarna , mające związane właściwości musżą dostarczyć metody przyłączania słuchaczy zmian właściwości: addPropertyChangeListener(PropertyChangeListener)
Klasy-ziarna, mające ograniczane właściwości, muszą dostarczyć metody addVetoableChangeListener(VetoableChangeListener).
Ziarna możemy wykorzystywać, możemy też je tworzyć (w znaczeniu: definiować klasę ziarna)
Tworzenie ziarna (jako klasy) wymaga zdefiniowania klasy, która:
- stosuje ogólnie przyjęte wzorce sygnatur metod i/lub uzupełniona jest
przez dodatkową specjalną klasę opisującą "niestandardowe" informacje o ziarnie
(implementacja interfejsu BeanInfo)
- zapewnia serializację obiektów
- zawiera konstruktor bezparametrowy
- uzwględnia działania w środowisku wielowątkowym (do obiektu klasy może równocześnie odwoływać się kilka wątków)
2.6. Nasłuch i wetowanie zmian właściwosci
Zmiana właściwości związanej lub ograniczanej powinna generować zdarzenie typu PropertyChangeEvent.
Komponenty (obiekty) zainteresowane w śledzeniu zmian tej własciwości muszą implementować interfejs PropertyChangeListener. W ten sposób stają się słuchaczami zmian właściwości..
Komponenty, które mogą wetować zmiany właściwości muszą implementować interfejs VetoableChangeListener (będą więc słuchaczami zmian właściwości ograniczonych i będę miały możliwość wetowania tych zmian).
Zdarzenie typu PropertChangeEvent możemy zapytać o:
- nazwę właściwości - String getPropertyName()
- starą wartość własciwości (przed zmianą) – Object getOldValue()
- nową wartość własciwości – Object getNewValue()
Interfejs PropertyChangeListener ma jedną metodę:
public void propertyChange(PropertyChangeEvent)
W implementacji tej metody, dowiadując się o zmianach właściwości, możemy na nie odpowiednio reagować.
Również interfejs VetoableChangeListener ma jedną metodę: vetoableChange(...)
z argumentem–zdarzeniem typu PropertyChange. W jej implementacji , gdy dowiemy
się już wszystkich niezbędnych szczegółow o zmianie – możemy ją zawetować.
Wetowanie zmiany odbywa się na zasadzie zgłoszenia wyjątku PropertyVetoException, zatem deklaracja metody vetoableChange wygląda następująco:
public void vetoableChange(PropertyChangeEvent e)
throws PropertyVetoException
a w jej implementacji – gdy po sprawdzeniu warości właściwości chcemy zgłosić veto – sygnalizujemy wyjątek: throw new PropertyVetoException(...).
Słuchacze zmian właściwości związanych (jak zawsze w Javie) musżą
być przyłączeni do źródła zdarzenia., którym jest w tym przypadku ziarno.
Przyłączenie staje się możliwe, jesli w klasie-ziarnie zdefiniowano metodę addPropertyChangeListener(...)
Musi też być zdefiniowana metoda removePropertyChangeListener, odłaczająca słuchacza.
To samo dotyczy nasłuchu zmian właściwości ograniczanych: klasa zaiarno musi dostarczyć metody przyłączenia słuchacza: addVetoableChangeListener oraz metody odłączania słuchacza: removeVetoableChangeListener.
Obowiązkiem klasy ziarna, która implementuje związane i/lub ograniczane właściwości
jest również dostarczenie odpowiednich definicji setterów dla tych właściwości.
W setterach należey generować zdarzenie PropertyChangeEvent i propagować go pośród przyłączonych słuchaczy.
W pakiecie java.beans znajdują się dwie klasy narzędziowe, znacznie ułatwiające
wykonanie tych zadań: PropertyChangeSupport i VetoableChangeSupport.
Klasy dostarczają metod generowania zdarzeń i propagacji zdarzeń zmian:
- właściwości związanych - firePropertyChange
- właściwości ograniczanych - fireVetoableChange słuchaczy.
- a także metod przyłączanai i odłączania słuchaczy tych zmian (addNNNListener, removeNNListener).
Konstruktory tych klas mają jako argument referencję do obiektu-ziarna.
Schemat postępowania przy implementacji właściwości związanej jest następujący:
class Ziarno .... {
//wsparcie
private PropertyChangeSupport chg = new PropertyChangeSupport(this);
String text; // to będzie właściwośc związana o nazwie "text"
...
// setter
synchronized void setText(String newTxt) { // pamiętamy o wielowątkowości!
String oldTxt = text; // stara wartość
text = newTxt; // ustalenie nowej wartości
// powiadomienie
chg.firePropertyChange("text", oldTxt, newTxt);
}
....
// metody dodawania i usuwania słuchaczy
public synchronized void addPropertyChangeListener(PropertyChangeListener l) {
chg.addPropertyChangeListener(l);
}
public synchronized void removePropertyChangeListener(PropertyChangeListener l) {
chg.removePropertyChangeListener(l);
}
...
}
Implementacja właściwości ograniczanych musi uzwględniać możliwość zawetowania zmiany przez któregoś ze słuchaczy.
Schemat postępowania przy implementacji właściwości ograniczanej jest następujący:
class Ziarno .... {
//wsparcie
private VetoableChangeSupport veto = new VetoableChangeSupport(this);
String text; // to będzie właściwośc ograniczana o nazwie "tekst"
...
// setter
synchronized void setText(String newTxt) throws PropertyVetoException
{
String oldTxt = text; // stara wartość
// wywołujemy metodę fireVotoableChange, która z kolei
// wywołuje metody vetoableChange zarejestrowanych słuchaczy
// jeśli któraś z nich zgłasza veto, setter kończy działanie
// a wyjątek PropertyVetoException jest przekazywany do obsługi
// przez metodę wywołującą setText
veto.fireVetoableChange("tekst", oldTxt, newTxt);
// Tylko jeśli nikt nie zawetował zmiany:
text = newTxt; // ustalenie nowej wartości
}
....
// metody dodawania i usuwania słuchaczy
public synchronized void addVetoableChangeListener(PropertyChangeListener l) {
veto.addVetoableChangeListener(l);
}
public synchronized void removeVetoableChangeListener(PropertyChangeListener l) {
veto.removeVetoableChangeListener(l);
}
...
}
// Uwaga: wsystkie klasy zdarzeniowe i interfejsy nasłuchu
// dla właściwości znajdują się w pakiecie java.beans.
2.7. JavaBean - przykład praktyczny
Będziemy budować klasę-licznik jako JavaBean. Klasę nazwiemy Counter.
Licznik będzie miał jedną właściwość o nazwie count (stan licznika).
W wersji pierwszej uczynimy tę właściwość związaną (bounded), w wersji drugiej – związaną i ograniczoną (constrained).
Przy budowie aplikacji zastosujemy koncepcję "Model-View-Controller".
Sama klasa Counter odzwierciedla logikę działania licznika ("model
"). Obiekty tej klasy "są niewidzialne", a zatem żeby zobaczyć licznik musimy
stworzyć dodatkową klasę, która zdefiniuje widok licznika (view).
Nazwiemy ją CounterView. Warto zwrócić uwagę: separacja kodu jest
korzystna – widok uniezależniamy od modelu, a model od widoku, w ten sposób
możemy mieć np. wiele widoków licznika, lub zmieniać model nie zmieniając
widoku.
Komunikacja między modelem i widokiem będzie się odbywać na zasadzie nasłuchu
zmian właściwości (zmian właściwości count) czyli obiekt klasy CounterView
będzie też słuchaczem zmian właściwości (PropertyChangeListener).
Musimy też mieć jakieś środki zmiany stanu licznika. Interakcję użytkownika z modelem/widokiem zapewnia tzw. kontroler.
Widzieliśmy, że w komponentach Swingu (w naturalny dla nich sposób) kontroler
połączony jest z widokiem. Tu jednak odseparujemy jego kod od widoku, tworząc
klasę CounterControlGui, zapewniającą interfejs interakcji z licznikiem.. Widok zostanie dodany do tego GUI (ale kody obu klas będą odseparowane).
W wersji drugiej – kiedy właściwość count będzie związana i ograniczana musimy
dostarczyć obiektu-nadzorcy, który będzie sprawdzał czy zmiana właściwości
jest dopuszczalna i jeśli stwierdzi, że nie – będzie wetował tę zmianę. Odpowiednią
klasę nazwiemy CounterLimitator.
W metodzie main(...) klasy Main, w której nasza aplikacja zacznie życie stworzymy
wszystkie odpowiednie obiekty w/w klas i ustanowimy niezbędne połączenia
między nimi.
W sumie logika działania aplikacji będzie wyglądać tak:
Zaczynamy od wersji pierwszej, uboższej, w której klasa Counter daje przykład
typowego programowania JavaBean z wlaściwością związaną:
// Klasa Counter
import java.awt.event.*;
import java.beans.*;
import java.io.*;
public class Counter implements Serializable {
private int count = 0; // właściwość count
// Pomocniczy obiekt do prowadzenia listy słuchaczy zmian właściwości oraz
// propagowania zmian wśród zarejestrowanych złuchaczy
private PropertyChangeSupport propertyChange = new PropertyChangeSupport(this);
// Konstruktory
public Counter() {
this(0);
}
public Counter(int aCount) {
setCount( aCount );
}
// Metody przyłączania i odłączania słuchaczy zmian właściwości
public synchronized void addPropertyChangeListener(PropertyChangeListener listener) {
propertyChange.addPropertyChangeListener(listener);
}
public synchronized void removePropertyChangeListener(PropertyChangeListener l) {
propertyChange.removePropertyChangeListener(l);
}
// Proste metody zwiększania i zmniejszania licznika
public void increment() {
setCount(getCount()+1);
}
public void decrement() {
setCount(getCount()-1);
}
// Getter właściwości "count"
public int getCount() {
return count;
}
// Setter właściowści "count"
public synchronized void setCount(int aCount) {
int oldValue = count;
count = aCount;
// wywołanie metody firePropertChange z klasy PropertyChangeSupport
// powoduje wygenerowanie zdarzenia PropertyChangeEvent i rozpropagowanie
// go wśród wszystkich przyłączonych słuchaczy, ale tylko wtedy, gdy nowa
// wartość właściwości różni się od starej wartości
propertyChange.firePropertyChange("count", new Integer(oldValue),
new Integer(aCount));
}
}
Widok licznika przedstawimy jako etykietę - klasa ta jednocześnie będzie
nasłuchiować zmian właściwości count i odpowiednio do tego zmieniać tekst
na etykiecie (a także wyprowadzać informacje o zmianach właściwości count
na konsolę).
//Klasa CounterView
//Widok licznika przedstawiamy w postaci etykiety
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.beans.*;
public class CounterView extends JLabel implements PropertyChangeListener {
// Konstruktor domyślny: inicjalizuje etykietę tekstem "0"
CounterView() {
this("0");
}
// Konstruktor inicjalizujący etykietę podanym tekstem
CounterView(String lab) {
super(lab);
setOpaque(true); // etykieta nie przezroczysta
// ramka
setBorder(BorderFactory.createLineBorder(Color.black));
// rozmiary i wyrównanie tekstu
setPreferredSize(new Dimension(75, 40));
setHorizontalAlignment(CENTER);
}
// obługa zdarzenia PropertyChange
public void propertyChange(PropertyChangeEvent e) {
Integer oldVal = (Integer) e.getOldValue(),
newVal = (Integer) e.getNewValue();
System.out.println("Value changed from " + oldVal + " to " + newVal);
setText("" + newVal + ""); // pokazanie na etykiecie nowego stanu licznika
}
}
Klasa kontrolera - CounterControlGui dostarcza dwóch przycisków (zwiększ,
zmniejsz licznik) oraz pole tekstowe, w którym można wpisać wartość licznika
(ENTER)
Zarówno kliknięcie w przyciski jak i ENTER na polu tekstowym powoduje powstanie
zdarzenia Action, które tu (w tej klasie) obsługujemy ustalając nowe wartości
licznika za pomocą metod incremet() decrement() i setCount(...) z klasy Counter.
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.beans.*;
import java.io.*;
public class CounterControlGui extends JFrame implements ActionListener {
Counter counter;
JButton binc = new JButton("Increment");
JButton bdec = new JButton("Decrement");
JTextField txt = new JTextField(10);
// Konstruktor otrzymuje jako argumenty obiekty typu Counter i CounterView
// Pierwszy jest nam potrzebny do komunikacji z licznikiem, drugi - widok
// wbudujemy w to GUI.
CounterControlGui(Counter c, CounterView clab) {
counter = c;
Container cp = getContentPane();
cp.setLayout(new FlowLayout());
binc.addActionListener(this);
cp.add(binc);
cp.add(clab);
bdec.addActionListener(this);
cp.add(bdec);
txt.addActionListener(this);
cp.add(txt);
setDefaultCloseOperation(3);
pack();
show();
}
// Obsługa akcji
public void actionPerformed(ActionEvent e) {
if (e.getSource() == txt) {
int n = 0;
try {
n = Integer.parseInt(txt.getText());
} catch (NumberFormatException exc) { return; }
counter.setCount(n);
return;
}
String cmd = e.getActionCommand();
if (cmd.equals("Increment")) counter.increment();
else if (cmd.equals("Decrement")) counter.decrement();
else System.out.println("Unrecognized command");
}
}
I wreszcie lacząca wszystko klasa Main, która inicjuje działanie aplikacji.
public class Main {
public static void main(String[] args) {
// Tworzymy obiekty: licznik i jego widok
Counter counter = new Counter();
CounterView counterView = new CounterView(""+counter.getCount());
// Rejestrujemy widok jako słuchacza zmian licznika
counter.addPropertyChangeListener(counterView);
// Tworzymu GUI kontrolera i pokazujemy go
CounterControlGui gui = new CounterControlGui(counter, counterView);
gui.pack();
gui.show();
}
}
Dizałanie aplikacji ilustruje rysunek oraz komunikaty na konsoli, powstałe
po kolejnych klilnięciach w przyciski Incremenet i Decrement oraz wprowadzeniu
liczby 23 w polu tekstowym i naciśnieciu ENTER.
Value changed from 0 to 1
Value changed from 1 to 0
Value changed from 0 to 23
Wersja 2
W wersji drugiej chcemy dodać obiekt – nadzorcę, który będzie sprawdzał czy
zmiana licznika jest dopuszczalna, a jeśli nie to będzie wetował tę zmianę.
Mechanizm wetowania polega na sygnalizowaniu wyjątku PropertyVetoException.
Wyjątek ten będzie sygnalizowany przez obiekt-nadzorcę, który jest jednoczesnie
słuchaczem zmian wartości właściwości ograniczonych (VetoableChangeListener).
Klasa definiująca obiekty nadzorujące nasz licznik wygląda tak.
import java.beans.*;
public class CounterLimitator implements VetoableChangeListener {
// minimalne i makszymalne dopuszczalne wartości licznika
private int min, max;
CounterLimitator(int minLim, int maxLim) {
min = minLim;
max = maxLim;
}
// Obsługa zdarzenia vetoableChange
// metoda może sygnalizować PropertyVetoException
public void vetoableChange(PropertyChangeEvent e)
throws PropertyVetoException {
Integer newVal = (Integer) e.getNewValue();
int val = newVal.intValue();
// Sprawdzamy, czy zmiana licznika jest dopuszczalna,
// jeśli nie – sygnalizujemy wyjatek PropertyVetoException
if (val < min || val > max)
throw new PropertyVetoException("Niedopuszczalna zmiana wartości", e);
}
}
W klasie Counter musimy poczynić zmiany, po to by właściwość count była zarazem związana i ograniczana.
Wykorzystamy podobną do PropertyChangeSupport klasę pomocniczą VetoableChangeSupport
Obiekt tej klasy nazwiemy vetos, dostarczymy też metod przyłączania i odłączania słuchaczy zmian ograniczanych.
public class Counter {
//...
private VetoableChangeSupport vetos = new VetoableChangeSupport(this);
public synchronized void addVetoableChangeListener(VetoableChangeListener l) {
vetos.addVetoableChangeListener(l);
}
public synchronized void removeVetoableChangeListener(VetoableChangeListener l) {
vetos.removeVetoableChangeListener(l);
}
// ...
}
Zmianie ulegnie też metoda setCount.
public class Counter {
//...
public synchronized void setCount(int aCount)
throws PropertyVetoException {
int oldValue = count;
// wywołujemy metodę fireVotoableChange, która z kolei
// wywołuje metody vetoableChange zarejestrowanych słuchaczy
// jeśli któraś z nich zgłasza veto, setter kończy działanie
// a wyjątek PropertyVetoException jest przekazywany do obsługi
// przez metodę wywołującą setCount
// (co zaznaczyliśmy w nagłówku metody przez
// throws PropertyVetoException)
vetos.fireVetoableChange("count", new Integer(oldValue), new Integer(aCount));
// tylko jeśli nikt nie zawetował
count = aCount; // ustalamy nową wartość licznika
// ... powiadamiamy nasłuchujących zmiany właściwości count
propertyChange.firePropertyChange("count", new Integer(oldValue),
new Integer(aCount));
}
//...
}
Ponieważ metody increment, decrement i konstruktory wywołują metodę setCount,
to w sygnaturach tych metod i konstruktorów musimy dodać informację, że wyjątek
PropertyVetoException ma być obsługiwany przez wołającego te metody.
A wołamy je z GUI kontrolera.
Tam jest miejsce do obsługi tego wyjątku:
public class CounterControlGui .... {
....
public void actionPerformed(ActionEvent e) {
try {
if (e.getSource() == txt) {
int n = 0;
try {
n = Integer.parseInt(txt.getText());
} catch (NumberFormatException exc) { return; }
counter.setCount(n);
return;
}
String cmd = e.getActionCommand();
if (cmd.equals("Increment")) counter.increment();
else if (cmd.equals("Decrement")) counter.decrement();
else System.out.println("Unrecognized command");
} catch (PropertyVetoException exc) { // obługa wyjatku:
System.out.println(""+ exc); // podanie informacji
//o niedopuszczalnej zmianie wartości
}
}
//...
}
W klasie Main dodajemy fragment dotyczący tworzenia obiektu klasy CounterLimitator
(nadzorcy) i rejestrujemy go jako słuchacza zmian właściwości ograniczanej:
import java.beans.*;
public class Main {
public static void main(String[] args) throws PropertyVetoException {
Counter counter = new Counter();
CounterView counterView = new CounterView(""+counter.getCount());
counter.addPropertyChangeListener(counterView);
// licznik może się zmieniać od –5 do 10
// bo Limitator zawetuje każdą inną zmianę
CounterLimitator clim = new CounterLimitator(-5, 10);
counter.addVetoableChangeListener(clim);
CounterControlGui gui = new CounterControlGui(counter, counterView);
gui.pack();
gui.show();
}
}
Dzialanie programu (przy tym samym GUI kontrolera) zilustrujemy komunikatami
na konsoli po kolejneych kliknięciach w przycisk "Decrement".
Value changed from 0 to -1
Value changed from -1 to -2
Value changed from -2 to -3
Value changed from -3 to -4
Value changed from -4 to -5
java.beans.PropertyVetoException: Niedopuszczalna zmiana wartości
Jak widać, CounterLimitator nie dopuścił do zmiany wartości z -5 na -6.
2.8. Introspekcja
Srodowisko programistyczne, które ma umożliwiać dynamiczne odczytywanie własności
i funkcjonalności dowolnych "podrzucanych" mu ziaren analizuje ziarna za
pomocą uniwerslanych metod introspekcji.
Introspekcja kojarzy dwa mechanizmy:
- analizę komponentów za pomocą metod refleksji przy założeniu, że stosowane
są pewne standardowe wzorce nazewnictwa, umożliwiające określenie własnściwości
i ich typu, metod pobierania i ustalania tych właściwości, rodzajóa zdarzeń,
metod rejestracji słuchaczy i nnych metod udostępnianych przez dane ziarno
"na zewnątrz"
- określanie uzewnętrznianych właściwości i funkcjonalności ziarna na
podstawie dowolnie specyfikowanych przez twórcę ziarna elementów, zapisywanych
w odpowiedniej dla danego ziarna klasie BeanInfo.
Oba mechanizmy mogą być stosowane łącznie. W prostych przypadkach (prostych
ziaren) nie ma potrzeby żmudnego tworzenia klasy BeanInfo, metody refleksji
są całkiem wystarczające.
Introspekcja za pomocą metod refleksji jest możliwa dzięki kontraktowi dotyczącemu wzorców nazewnictwa.
Dla właściwości introspekcja określa pary metod get... (is...) - set...
z tymi samymi nazwami właściwości i z odpowiednimi sygnaturami. Może się
okazać, że niektóre właściwości są tylko do odczytu lub tylko do zapisu.
Uwzględnia się też indeksowane własciwości.
Dla określenia, czy istnieją możliwość obsługi zdarzeń i jakich używane są wzorce: addXXXListener i removeXXXListener.
Samą introspekcje realizuje klasa Introspector.
Introspector analizuje klasę-zarna (i nadrzędne wobec niej klasy oraz implementowane
interfejsy) zbierając informacje o właściowościach, metodach, zdarzeniach.
Informacja ta jest umieszczana w obiekcie typu BeanInfo (BeanInfo jest nazwą interfejsu). Wobec tego obiektu możemy następnie zastosować metody zwracające iinformacje o ziarnie.
Np. analizę klasy-ziarna javax.swing.JButton uzyskujemy przez następujące odwołanie:
BeanInfo info = Introspector.getBeanInfo(Class.forName("javax.swing.JButton"));
lub
BeanInfo info = Introspector.getBeanInfo(javax.swing.JButton.class));
Następnie wobec obiektu info możemu zastosować metody interfejsu BeanInfo:
- EventSetDescriptor[] getEventSetDescriptors() - zwraca tablicę deskryptorów zdarzeń
- PropertyDescriptor[] getPropertyDescriptors() - zwraca tablicę deskryptorów własności
- MethodDescriptor[] getMethodDescriptors() - zwraca tablicę deskryptorów metod
- dla każdgo deskryptora metody ParameterDescriptor[] getParameterDexriptors() - zwraca tablicę deskryptorów parametrów tej metody
Zwracane tablice są tablicami obiektów odpowiednich klas. Wobec tych obiektów
stosujemy metody tych klas pozwalające na uzyskiwanie różnej konkretnej informacji.
Np. program na poiniższym wydruku analizuje klasę podaną jako argument i wypisuje niektóre informacje o niej:
import java.lang.reflect.*;
import java.beans.*;
public class BeanAnalyze {
static void say(String s) { System.out.println(s); }
public static void main(String[] arg) throws Exception {
BeanInfo beanInfo = Introspector.getBeanInfo(Class.forName(arg[0]));
PropertyDescriptor[] pd = beanInfo.getPropertyDescriptors();
MethodDescriptor[] md = beanInfo.getMethodDescriptors();
EventSetDescriptor[] evd = beanInfo.getEventSetDescriptors();
say("Właściwości:");
for (int i = 0; i < pd.length; i++) {
say(pd[i].getShortDescription());
// getReadMethod i getWriteMethod zwracają obiekty typu Method
say(" getter: "+ pd[i].getReadMethod());
say(" setter: "+ pd[i].getWriteMethod());
}
say("\nMetody:");
for (int i=0; i<md.length; i++) {
say(" " + md[i].getMethod());
}
say("\nZdarzenia:");
for (int i = 0; i < evd.length; i++) {
say("Zdarzenie : " + evd[i].getShortDescription());
Method[] met = evd[i].getListenerMethods();
say("Metody obsługi:");
for (int j=0; j < met.length; j++) say(" " + met[j]);
}
}
}
Fragmenty (dużego!) wydruku z programu uruchomionego z argumentem javax.swing.JButton:
Właściwości:
UI
getter: public javax.swing.plaf.ButtonUI javax.swing.AbstractButton.getUI()
setter: public void javax.swing.AbstractButton.setUI(javax.swing.plaf.ButtonUI)
UIClassID
getter: public java.lang.String javax.swing.JButton.getUIClassID()
setter: null
accessibleContext
getter: public javax.accessibility.AccessibleContext javax.swing.JButton.getAccessibleContext()
setter: null
action
getter: public javax.swing.Action javax.swing.AbstractButton.getAction()
setter: public void javax.swing.AbstractButton.setAction(javax.swing.Action)
actionCommand
getter: public java.lang.String javax.swing.AbstractButton.getActionCommand()
setter: public void javax.swing.AbstractButton.setActionCommand(java.lang.String)
actionListeners
getter: public java.awt.event.ActionListener[] javax.swing.AbstractButton.getActionListeners()
setter: null
actionMap
getter: public final javax.swing.ActionMap javax.swing.JComponent.getActionMap()
setter: public final void javax.swing.JComponent.setActionMap(javax.swing.ActionMap)
...
Metody:
public boolean java.awt.Component.action(java.awt.Event,java.lang.Object)
public synchronized void java.awt.Component.add(java.awt.PopupMenu)
public java.awt.Component java.awt.Container.add(java.awt.Component)
public java.awt.Component java.awt.Container.add(java.awt.Component,int)
public void java.awt.Container.add(java.awt.Component,java.lang.Object)
public void java.awt.Container.add(java.awt.Component,java.lang.Object,int)
public java.awt.Component java.awt.Container.add(java.lang.String,java.awt.Component)
public void javax.swing.AbstractButton.addActionListener(java.awt.event.ActionListener)
...
Zdarzenia:
Zdarzenie : action
Metody obsługi:
public abstract void java.awt.event.ActionListener.actionPerformed(java.awt.event.ActionEvent)
Zdarzenie : ancestor
Metody obsługi:
public abstract void javax.swing.event.AncestorListener.ancestorMoved(javax.swing.event.AncestorEvent)
public abstract void javax.swing.event.AncestorListener.ancestorAdded(javax.swing.event.AncestorEvent)
public abstract void javax.swing.event.AncestorListener.ancestorRemoved(javax.swing.event.AncestorEvent)
Zdarzenie : change
Metody obsługi:
public abstract void javax.swing.event.ChangeListener.stateChanged(javax.swing.event.ChangeEvent)
...
Szczerze mowiąc, za pomocą metod instrospekcji sporo można się dowiedziec o dostępnych właściwościach i metodach danej klasy.
Oczywiście, mając obiekty-metody (uzyskane z deskryptorów metod) mozemy je
dynamicznie wyowływac środkami refleksji. Ale istnieje też nieco prostsza,
specjalnie dla JavaBean przygotowana możliwość dynamicznego pobierania i
ustalania wlaściwości oraz wołania innych metod.
2.9. Dynamiczne pobieranie i ustalanie właściwości
W pakiecie java.beans znajdziemy dwie ciekawe klasy Statement i Expression
Obiekt klasy Statement tworzymy za pomocą konstuktora:
Statement s = Statement( Object target,
String methodName,
Object[] arguments );
a wywołanie na jego rzecz metody void execute() spowoduje wywołanie na rzecz obiektu target metody o nazwie methodName z argumentami podanymi w tablicy arguments
Zwykle stosujemy tę procedure do ustalania właściowości JavaBean, ale może
ona być rownież stosowana do wyołania dowolnych metod
Klasa Expression dziedziczy klasę Statement. Czyli tu też możemy wołać metodę
execute(). Jednak jej główne zastosowanie polega na dynamicznym pobieraniu
właściwości, lub - inaczej - dynamicznym wołaniu metod, które zwracają wyniki
i uzyskiwaniu dostępu do tych wyników.
Obiekt klasy Expression tworzymy za pomocą konstuktora:
Expression e = Expression( Object target,
String methodName,
Object[] arguments );
-
wywołanie na jego rzecz metody void execute() spowoduje wywołanie
na rzecz obiektu target metody o nazwie methodName z argumentami podanymi
w tablicy arguments oraz przechowanie wyniku wywołanej metody w obiekcie
Expresion; wynik ten będzie dostępny przez wywolanie metody Object getValue()
na rzecz obeiktu Expressiom
- samo wywołanie metody Object getValue() spowoduje ten sam efekt o ile wynik dla danego obiektu-wyrażenia nie został jeszcze ustalony.
Szczegółowo komentowany program ilustruje zastosowanie tych klas.
W poniższym przykładzie metody klas Statement i Expression bedziemy stosowac
wobec obiektów klasy JButton oraz naszej własnej klasy TestBean, która wygląda
tak:
public class TestBean {
private String[] headers;
private int count;
public TestBean() {
}
public TestBean(int n) {
count = n;
}
public String[] getHeaders() {
return headers;
}
public void setHeaders(String[] value) {
headers = value;
}
public int getCount() {
return count;
}
public void setCount(int value) {
count = value;
}
}
Szczegółowo komentowany program ilustruje zastosowanie klas Statement i Expression,
dając też pewne dodatkowe na ich temat informacje.
import java.beans.*;
import java.awt.*;
import javax.swing.*;
public class DynamicExec {
public static void main(String[] args) throws Exception {
Statement stmt;
Expression expr;
JButton b = new JButton();
// Na rzecz przycisku wołamy dynamicznie metodę setText
// z argumentem "Przycisk"
stmt = new Statement(b, "setText", new Object[] { "Przycisk" });
stmt.execute();
// Jaki wynik? Najpierw statyczne odwołanie
System.out.println("Tekst na przycisku 1: " + b.getText());
// Teraz dynamicznie: stwórzmy wyrażenie, którego wynikiem
// jest wynik podanej metody z podanymi argumentami wywolanej
// na rzecz b
// Uwaga: brak argumentów - czyli tablica Object o rozmiarze 0
expr = new Expression(b, "getText", new Object[0]);
// Jeżeli wyrażenie expr nie ma jeszcze wyniku
// metoda getValue() wywołuje podaną w wyrażeniu metodę
// i zwraca jej wynik; w przeciwnym razie zwraca
// ustalony wczesniej wynik
String txt = (String) expr.getValue();
System.out.println("Tekst na przycisku 2: " + txt);
// Możemy też stosować klasy Statement i Expression
// wobec naszych własnych klas JavaBeans
TestBean tbean = new TestBean();
// Uwaga: przy przekazywaniu argumentów i zwrocie wynikow
// następują automatyczne przeksztalcenia pomiedzy
// typami prostymi i odpowiadającymi im klasami opakowującymi
// np. int - Integer - setCount wymaga argumentu int,
// my podajemy Integer
stmt = new Statement(tbean, "setCount",
new Object[] { new Integer(22) });
stmt.execute();
// Jaka jest teraz wartość właściwości count
// I znowu: getCount() zwraca int, my odbieramy Integer
expr = new Expression( tbean, "getCount", new Object[0] );
Integer val = (Integer) expr.getValue();
System.out.println("Wartość count: " + val);
// Czy możemy działać na tabliach? Ależ tak!
stmt = new Statement(tbean, "setHeaders",
new Object[] { new String[] { "a", "b" } }
);
stmt.execute();
expr = new Expression(tbean, "getHeaders", new Object[0]);
String[] hdr = (String[]) expr.getValue();
System.out.println("Ustalone nagłówki");
for (int i=0; i<hdr.length; i++)
System.out.println(hdr[i]);
// Możemy nawet stworzyć nowy obiekt
// używając specjalnej nazwy metody - new (oczywiście)
expr = new Expression(TestBean.class, "new",
new Object[] { new Integer(111) }
);
TestBean tb2 = (TestBean) expr.getValue();
expr = new Expression (tb2, "getCount", new Object[0]);
val = (Integer) expr.getValue();
System.out.println("W nowym obiecie count = " + val);
}
}
2.10. Serializacja JavaBeans
Oprócz zapisywania do strumieni obiektowych (ObjectOutputStream), obiekty
JavaBeans można serializowac w postaci tekstowej, w formacie XML (wersja
1.0, kodowanie UTF-8). Jest to nawet - w przypadku JavaBeans - bardziej "przenośny"
sposób utrwalania obiektów.
Zapisywaniem obiektów zajmuje się klasa XMLEncoder, a ich odtwarzaniem -
klasa XMLDccoder. Przy tym stan obiektu zapisywany jest w postaci zdatnej
do wykorzystania przez klasy Statement i Expression przy odtwarzaniu tego
obiektu
Najprostsze (przeznaczone wyłącznie dla JavaBeans) zastosowanie tych klas pokazuje poniższy program.
import java.beans.*;
import java.awt.*;
import javax.swing.*;
import java.io.*;
public class SerialBean {
String fname = "test.xml";
public SerialBean() {
JButton b = new JButton("Kąśliwie wróbel ćwierkał");
b.setBackground(Color.red);
b.setForeground(Color.yellow);
try {
XMLEncoder enc = new XMLEncoder(
new BufferedOutputStream(
new FileOutputStream(fname)
)
);
enc.writeObject(b);
enc.close();
} catch (FileNotFoundException exc) {
exc.printStackTrace();
System.exit(1);
}
nowReadAndReport();
}
private void nowReadAndReport() {
try {
XMLDecoder dec = new XMLDecoder(
new BufferedInputStream(
new FileInputStream(fname)));
Object obj = dec.readObject();
JButton b = (JButton) obj;
dec.close();
System.out.println("Napis na przycisku: " + b.getText());
System.out.println("Kolor tła: " + b.getBackground());
System.out.println("Kolor tekstu : " + b.getForeground());
} catch (FileNotFoundException exc) {
exc.printStackTrace();
System.exit(1);
}
}
public static void main(String[] args) {
SerialBean serialbean = new SerialBean();
}
}
Wyprowadzi on na konsolę właściwą informację o stanie zapisanego a później odtworzonego obiektu:
Napis na przycisku: Kąśliwie wróbel ćwierkał
Kolor tła: java.awt.Color[r=255,g=0,b=0]
Kolor tekstu : java.awt.Color[r=255,g=255,b=0]
Z ciekawości można zajrzeć do pliku test.xml.
<?xml version="1.0" encoding="UTF-8"?>
<java version="1.4.1-rc" class="java.beans.XMLDecoder">
<object class="javax.swing.JButton">
<string>KÄĹ>liwie wrĂłbel Ä+wierkaĹ'</string>
<void property="background">
<object class="java.awt.Color">
<int>255</int>
<int>0</int>
<int>0</int>
<int>255</int>
</object>
</void>
<void property="foreground">
<object class="java.awt.Color">
<int>255</int>
<int>255</int>
<int>0</int>
<int>255</int>
</object>
</void>
</object>
</java>
Faktycznie, jest do dokładny szablon gotowy do zastosowania klas Expression
(dla metody-kostsruktora new z argumentem String) i Statement (dla setBackground
i setForeground). Wszystko opiera się na zachowaniu protokołu JavaBeans.
Oczywiście, możemy w ten sposób serializować obiekty prawie wszystkich klas
Javy (bo prawie wszystkie są JavaBeans) oraz własnych klas, spełniających
protokół JavaBeans.
A nawet można uwzględnić pewne odstępstwa od tego
protokołu (np. inicjację właściwości JavaBeans w konstruktorz, bez użycia
setterów) poprzez dostosowanie delegata "persystencji", do którego odwołuje
się XMLEncoder - klasy DefaultPersistanceDelegate.
O takim bardziej zaawansowanym zastosowaniu, jak również o użyciu nasłuchu wyjątków dekodowania za pomocą interfejsu ExceptionListener można przeczytać w dokumentacji API Javy.
2.11. Inne zagadnienia związane z JavaBeans
To skrótowe wprowadzenie do JavaBeans nie wyczerpuje tematu.
Warto więc na koniec zwrócić uwagę na nieporuszone tu ważne kwestie
- przystosowanie ziaren: klasy-edytory właściwości używane m.in. w środowiskach wizualnych
- tworzenie klas BeanInfo, opisujących informacje niedostępną metodami refleksji,
- koteksty (BeanContext) - swoiste kontenery, dostarczające generalnych mechanizmów i serwisów dla JavaBeans.
Informacje na te teamaty można zanaleźć w dokumentacji.