OK, ten wpis już ma parę lat, stąd
część o serializacji doczekała się nowszej wersji ;-)
Dziś o dwóch nie do końca zrozumiałych dla wszystkich aspektach Java'y, czyli o odbiciach aka refleksjach, i o serializacji.
Zacznijmy od odbić/refleksji.
Co to jest? Jest to mechanizm, który pozwala nam na dostęp do metod i pól dowolnych obiektów do których posiadamy referencje [lub klas jeśli chodzi o metody i pola statyczne], oraz na używanie obiektów których definicji nie znamy w momencie pisania naszego kodu.
Wszystkie zabawy z odbiciami odbywają się przez metody klasy Class.
Chcąc załadować definicję klasy, która jest na ścieżce ClassLoadera, a której typu nie znamy w momencie tworzenia kodu używamy metody
forName(String)
. Najbardziej znanym przypadkiem gdy używamy tej metody jest ładowanie sterowników JDBC, które wygląda zwykle tak [tutaj przykład dla jakiegoś tam sterownika JDBC dla MySQLa]:
Class.forName("com.mysql.jdbc.Driver");
Java ładuje klasę
com.mysql.jdbc.Driver
, ale w tym przypadku nie zapisujemy nigdzie referencji do obiektu klasy [nie robimy tego bo sterownik JDBC sam rejestruje się w mechanizmach JDBC].
Zwykle potrzebujemy jednak tej referencji, czyli wołamy kod taki jak ten:
Class clasz = Class.forName("java.lang.String");
Od tego momentu w
clasz
mamy referencję do obiektu typu
Class
klasy
String
. Akurat w przypadku klas które znamy lub obiektów do których chcielibyśmy uzyskać dostęp łatwiej i rozsądniej jest używać pola class typu lub metody getClass() [obie rzeczy "dziedziczmy" po Object].
Czyli możemy to zrobić tak:
Class clasz1 = Class.forName("java.lang.String"); // ładujemy przy pomocy forName
Class clasz2 = String.class; // używamy pola class
Class clasz3 = "Test".getClass();
Mamy w końcu obiekt typu Class dzięki, któremu możemy uzyskać dostęp do bebechów klasy :-)
Po pierwsze możemy poprosić Java'ę o listę metod publicznych dostępnych w danej klasie przy pomocy metody
getMethods()
lub listę metod (wszystkich, czyli publicznych, domyślnych, chronionych i prywatnych) zadeklarowanych w danej klasie [ale już nie nadklasie] lub interfejsie [ale nie "nadinterjesie" ;-) choć tu oczywiście będą same metody publiczne co wynika z tego czym są interfejsy] przy pomocy metody
getDeclaredMethods()
. Jako wynik dostaniemy tablice
Method[]
. W tablicy będą wszystkie dostępne metody, jeżeli klasa ma metody "przeładowane" to każda z tych metod jest jednym elementem w tablicy.
Możemy poprosić też o metodę o zadanej nazwie i zadanej liście parametrów, i znów by dobrać się do metody o dowolnej widoczności w naszej klasie używamy
getDeclaredMethod(String,Class...)
, do dowolnej metody publicznej używamy
getMethod(String,Class...)
.
Gdzie pierwszy
String
to nazwa metody, a
Class...
to tablica parametrów.
Np. pobranie metody
compareTo(String)
z klasy
String
wygląda tak:
Class clasz3 = "Test".getClass();
Method method = clasz3.getMethod("compareTo",String.class);
Możemy w podobny sposób uzyskać też dostęp do pól [metody
getFields()
,
getDelcaredFields()
i adekwatne im metody z serii
getField(String)
], adnotacji, konstruktorów, etc.
Wróćmy do metod i obiektów.
Załadowaliśmy sobie definicję naszej klasy, teraz chcielibyśmy stworzyć instancję obiektu z tej klasy i zawołać sobie na nim jakieś metody. Jak to zrobić?
Tworzenie obiektu najprościej załatwić przez metodę
newInstance()
, której użycie jest równoważne wywołaniu konstruktora bezparametrowego. Stąd
new String()
jest równoważne
String.class.newInstance()
. [inna sprawa kto by wywoływał i po co konstruktor bezparametrowy
String
'a?]
Jeżeli chcemy użyć konstruktora z parametrami wygodniej nam będzie użyć metody
getConstructor(Class...)
lub
getDeclaredConstructor(Class...)
. Jak zwykle wersja bez słowa declared w nazwie będzie działać tylko dla publicznych konstruktorów [konstruktorów nie dziedziczymy więc będą to tylko konstruktory publiczne w danej klasie], wersja ze słowem declared w nazwie pozwala uzyskać referencję do konstruktora o dowolnej widoczności :-)
Poniżej przykład wywołania konstruktora
String(String)
:
Constructor c = String.class.getConstructor(String.class);
Object obj = c.newInstance("Toster"); // nie znamy typu więc używamy Object
System.out.println(obj);
Teraz gdy chcemy na tak utworzonym obiekcie wywołać jakieś metody musimy je pobrać przy pomocy pokazanych wyżej metod z rodziny getMethod.
Używamy do tego metody
invoke(Object,Object...)
z klasy
Method
:
Method m = obj.getClass().getMethod("compareTo", String.class);
Object result = m.invoke(obj,"parametr");
System.out.println(result);
Wyżej najpierw pobieramy referencję do metody
compareTo(String)
, a później wywołujemy ją w obiekcie
obj
i parametrem [akurat jest jeden] "parametr", wynik działania metody przypisujemy do zmiennej
result
.
W przypadku metod, czy pól prywatnych, domyślnych [gdy jesteśmy w innym pakiecie], czyli ogólnie niedostępnych wg. zasad widoczności Java'y musimy przed wywołaniem metody czy dostępem do pola zmienić jego widoczność [a możemy to zrobić przez odbicia :-)] przy pomocy metody
setAccessible(boolean)
danego cosia.
Jak znamy podstawy podstaw to trzeba o nich zapomnieć i wiedzieć, że wszystko wyżej jest złe i powinno być stosowane jak najrzadziej się da. W większości przypadków można używać odbić praktycznie bez ich używania ;-)
Często odbić chcemy używać do dodawania "wtyczek" do naszego kodu, znamy interfejs klasy której będziemy używać, nie mamy jednak na etapie pisania kodu dostępu do kodu, którego będzie używać nasza aplikacja jako wtyczek [co dość logiczne ;-)]. Wtedy zamiast używać tego wszystkiego co było wyżej najprościej zapewnić by wszystkie nasze klasy rozszerzały pewien interfejs, który posłuży nam do dostępu do "wtyczek", np. taki:
public interface Plugin {
onMessage(Message msg);
registerMessagesListener(MessagesListener);
}
Do tego każda nasza klasa powinna posiadać konstruktor bezparametrowy, wtedy praca z wtyczkami będzie wyglądała tak:
Class clasz = Class.forName(pluginClassName);
Plugin plugin = (Plugin)clasz.newInstance();
plugin.registerMessagesListener(this);
Dzięki takiemu podejściu odbić używamy tylko do stworzenia obiektu. Zaletą jest to, że po pierwsze o wiele łatwiej będzie nam wykryć błędy [bo jeśli pomylimy się w nazwie metody kompilator albo środowisko powie nam o tym od razu, a nie dopiero w trakcie działania], a sam kod będzie szybszy.
Sam w ciągu mojego życia programistycznego widziałem 3 przypadki użycia odbić gdy było to konieczne, z czego w jednym przypadku generowało to strasznie dużo problemów, w dwóch działało i chyba działa nadal bardzo dobrze.
Z racji oszczędności miejsca nie pisałem tutaj o wyjątkach związanych z refleksjami, dlatego zachęcam do zabawy z nimi :-)
Trochę to dłuższe wyszło niż zamierzałem ;-) Dlatego o serializacji będzie krótko.
Serializacja i deserializacja.
Co to jest? Jest to mechanizm pozwalający na zapisywanie stanu obiektów i odczytywanie ich. Dzięki temu możemy np. zapisać stan obiektów, po czym w momencie kolejnego uruchomienia aplikacji wczytać ich stan.
Częstym mitem jest to, że serializacja i deserializacja [głównie o nią w micie chodzi] możliwe są tylko dla obiektów z konstruktorami bezparametrowymi. Jest to fałszywy mit, bullshit, nieprawda, głupota, etc.
Serializacja i deserializacja są mechanizmem całkowicie niezależnym od tworzenia obiektów przy pomocy konstruktorów. Konstruktory nie są NIGDY wołane w trakcie deserializacji obiektu (nie jest to do końca prawda bo gdy piszemy własne metody do serializacji to nic nie stoi na przeszkodzie by używać konstruktorów). Serializacja i deserializacja działa tylko i wyłącznie dla obiektów utworzonych z klas, które implementują interfejs
Serializable
.
Serializable
to interfejs znacznikowy, który nie deklaruje żadnych metod.
Większość klas podstawowych z pakietu
java.lang
implementuje ten interfejs.
Dodatkowo serializacja i deserializacja wymaga by wszystkie pola inne niż prymitywy także implementowały interfejs
Serializable
lub były oznaczone jako
transient
.
Oznaczenie pola modyfikatorem
transient
mówi mechanizmowi serializacji by nie zachowywał wartości tego pola.
Dodatkowo mechanizm serializacji ignoruje pola statyczne.
Zapisanie stanu obiektu możliwe jest przy pomocy metody
writeObject(Object)
z klasy
ObjectOutputStream
, przykładowy kod może wyglądać tak:
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(str1);
W kodzie tym tworzymy strumień
ObjectOutputStream
na strumieniu w pamięci i zapisujemy do niego stan obiektu
str1
.
Odczyt jest równie prosty, używamy do niego metody
readObject()
z
ObjectInputStream
[tutaj uwaga,
readObject()
zwraca
Object
więc przy przypisaniu konieczne jest rzutowanie]:
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
String str2 = (String)ois.readObject(); // bardzo ważne rzutowanie
Proste prawda? Nie do końca ;-) Bo jest jeszcze cały mechanizm pozwalający na to by samemu zapisywać i odczytywać obiekty. Nie zawsze musimy albo możemy skorzystać ze standardowych mechanizmów, czasem możemy mieć np. jako pole w naszej klasie, którą chcielibyśmy serializować [dokładniej obiekty z niej utworzone] które jest typu, który nie jest serializowalny, a na dodatek nie mamy możliwości dodania mu tej właściwości. W takim przypadku możemy stworzyć własny sposób serializowania obiektów.
Służą nam do tego trzy metody [których deklarację kradnę z JavaDoc'a ;-)]:
private void writeObject(java.io.ObjectOutputStream out)
throws IOException;
private void readObject(java.io.ObjectInputStream in)
throws IOException, ClassNotFoundException;
private void readObjectNoData()
throws ObjectStreamException;
Metody te dodajemy w klasie, która ma być serializowalna [najlepiej tej która używa pola z którym są kłopoty].
Kłopotliwe pole zaznaczamy jako
transient
dzięki czemu standardowy mechanizm serializacji zignoruje to pole.
W metodzie
writeObject(ObjectOutputStream)
musimy oprogramować zapisywanie stanu naszego obiektu, nie musimy pisać obsługi dla wszystkich pól, ponieważ do tego możemy wykorzystać standardowy mechanizm z jego metodą
defaultWriteObject()
[którą wołamy na przekazanym jako parametr strumieniu], która "pojedzie" standardem, my musimy oprogramować tylko nasze problematyczne pole lub pola.
Jak łatwo się domyśleć metoda
readObject(ObjectInputStream)
służy do odczytywania stanu obiektu, i tu znów możemy użyć domyślnego mechanizmu, który może obsłużyć wszystkie pola z którymi nie ma kłopotów. Do tego celu używamy metody
defaultReadObject()
[znów wołanej na parametrze]. Ważne jest tu by pilnować kolejności, czyli albo najpierw wołamy w trakcie zapisu
defaultWriteObject()
, a później zapisujemy stan pól specjalnych, i w trakcie odczytu najpierw wołamy
defaultReadObject()
, a później odczytujemy pola specjalne, albo robimy na odwrót najpierw pisząc/zapisując pola specjalne, a później używając
defaultWriteObject()
/
defaultReadObject()
.
Ostatnią metodą jest
readObjectNoData()
, której przyznam się nigdy nie musiałem używać :-) więc mogę się w jej opisie oprzeć tylko na opisie z JavaDoc'a. W skrócie metoda ta jest wołana wtedy gdy "podnoszonej" klasy nie ma w strumieniu, który próbujemy odczytać...... ale przyznam, że nie udało mi się tego teraz oprogramować :-(
Są jeszcze dwie dodatkowe możliwe metody
writeReplace()
i
readResolve()
. Obie te metody służą do zastąpienia serializacji/deserializacj obiektu. Zamiast zapisywać nasz obiekt możemy przekazać do mechanizmu serializacji obiekt do zapisania przez zwrócenie go przez
writeReplace()
, a zamiast odczytywania obiektu ze strumienia możemy zwrócić obiekt przez
readResolve()
.
Ponieważ
ObjectOutputStream
i
ObjectInputStream
można zbudować praktycznie nad wszystkimi typami strumieni to możemy serializować obiekty do pamięci, plików, skompresowanych plików, przez sieć, etc. [tak btw. serializowanie do
GZIPOutputStream
na strumieniu "sieciowym" nie działa, tak samo deserializacja ;-)].
Mechanizm serializacji/deserializacji w Java'ie nie jest jakiś szczególnie bystry, problemy więc mogą się pojawiać często i gęsto gdy np. zapiszemy obiekt utworzony ze starszej wersji klasy, a będziemy próbowali go odczytać nowszą. Czasem mechanizm serializacji może sobie z tym poradzić, a my wolelibyśmy by wygenerował w takim przypadku wyjątek. Do tego możemy użyć znów niewymaganego ;-) pola
serialVersionUID
. Ma to swoje zalety, ponieważ standardowo Java sama "doda" takie pole i wystarczy drobna zmiana w kodzie klasy [jakakolwiek zmiana!] i nie będzie możliwości zdeserializowania obiektu, który został zserializowany poprzednią wersją.
No i to by były podstawy podstaw serializacji.
Po co jej używać? Np. można na niej łatwo zbudować prosty protokół komunikacji między elementami naszej aplikacji stojącymi na różnych maszynach, zapewnić "trwałość" aplikacji przez okresowe zapisywanie stanu aplikacji lub jej kluczowych obiektów na dysk, do replikacji danych między różnymi instancjami aplikacji, do przechowywania kosztownych do wyliczenia wartości i do czego dusza zapragnie.
Jajx, długie wyszło ;-) 2 godziny pisania :-)
Podobne postybetaSerializacja w Java'ie - revisited ;-)Lenistwo w działaniu, "piklujemy" Androida ;-)Czemu trzeba pomagać Jacksonowi? ;-)Sortujemy JTable gdy się da ;-)Sztuczki tropiciela błędów, part 2 ;-)