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");
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");
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();
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);
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);
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);
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);
}
Class clasz = Class.forName(pluginClassName);
Plugin plugin = (Plugin)clasz.newInstance();
plugin.registerMessagesListener(this);
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);
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
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;
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 postybeta
Serializacja 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 ;-)
Przy takich artykułach aż czytnik RSS ze szczęścia podskakuje ;] już wtedy wiedziałem, że będzie coś zacnego :) Dzięki ci wielkie za twój czas
OdpowiedzUsuńA masz jakieś sugestie co bym mogło się przydać opisać? ;-)
OdpowiedzUsuńto chyba pierwszy blog który dodałem do ulubionych :D
OdpowiedzUsuńŚwietny artykuł. Jeśli mogę chciałbym dodać 3 grosze o serializacji, którą niedawno zacząłem poznawać a dokładniej o ciekawym interfejsie Externalizable, o którym dowiedziałem się z Thinking in Java wyd.4 --> rozdział 18 Wejście-wyjście --> Kontrola serializacji.
OdpowiedzUsuń"Serializacja i deserializacja działa tylko i wyłącznie dla obiektów utworzonych z klas, które implementują interfejs Serializable." Poza Serializable jest jeszcze coś takiego jak interfejs Externalizable, który rozszerza Serializable dodając metody writeExternal(ObjectOutput out) i readExternal(ObjectInput in). Jego celem jest samodzielna kontrola sposobu serializacji klasy, która implementuje ten interfejs. Osiąga się ją poprzez odpowiednie zaimplementowanie wspomnianych już metod writeExternal() i readExternal(). W metodzie writeExternal(ObjectOutput out) na rzecz przekazywanego w argumencie obiektu ObjectOutput wywołuje się metody jak out.writeObject(skladowaTypuObiektowego),czy np. out.writeInt(skladowaTypuProstegoInt) by je zserializować. Analogicznie implementuje się readExternal(ObjectInput in), czyli np. skladowaTypuObiektowego = (KlasaSkładowej) in.readObject(); skladowaTypuProstegoInt=in.readInt(). Wypadałoby tu implementować te metody tak, aby kolejność odczytu składowych była taka jak kolejność ich zapisu. Jak widać dzięki temu interfejsowi sami decydujemy, które pola serializować, a które zostawić w spokoju więc nie ma konieczności użycia tu słówka kluczowego transient.
"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." Podejrzewam, że ten mit wziął się właśnie z interfejsu Externalizable, gdyż tam przy deserializacji wywoływany jest na początku domyślny konstruktor bezparametrowy deserializowanego obiektu (konstruktor musi być jednak publiczny) i dopiero po nim metoda readExternal() uzupełniająca jego pola składowe wcześniej zserializowanymi danymi. Przykład z Thinking in Java dostępny tu: http://www.java2s.com/Code/Java/File-Input-Output/Reconstructinganexternalizableobject.htm
Adak - Twoje 3 grosze są bardzo pomocne :) Dzięki!
OdpowiedzUsuńUwaga!
OdpowiedzUsuńW artykule jest błąd. Pola statyczne nie są ignorowane. Przynajmniej takie wnioski wyciągnąłem z krótkiego testu.
@Anonimowy - pola statyczne nie są serializowane, zrób taki test, stwórz klasę która jest Serializable, dodaj do niej pole statyczne typu String, ustaw mu wartość na "Test1", zserializuj obiekt, zmień wartość pola statycznego na "Test2", zdeserializuj obiekt, sprawdź wartość pola statycznego, będzie Test2.
OdpowiedzUsuńW tym co zserializujesz też nie będzie tekstu Test1.
Po prostu nie ma sensu serializować pól statycznych, gdyż one są właściwościami klasy, a nie obiektu.
Tak, wiem. Pomyłka w teście, który sobie zrobiłem :)
OdpowiedzUsuńWitam, super opisany mechanizm refleksji :) Bardzo prosiłbym o mały "wykładzik" o adnotacjach. Pewnie nie tylko ja mam z nimi problem :P
OdpowiedzUsuńA mnie ciekawi Przemku jak się ma, twoim zdaniem, serializacja do kodowania i dekodowania XML... Tam też przecież możesz zapisywać stany obiektów... Czy to chodzi tylko o kontrolę wersji obiektów czy jeszcze jest coś? Jak się ma do tego twoja praktyka?
OdpowiedzUsuńSzczerze nie rozumiem pytania :-)
UsuńCzy chodzi Ci o serializację/deserializację do/z XMLa?
W czym widzisz wyższość serializacji/deserializacji nad zapisywaniem stanów obiektów do XMLa przy (XMLEncoder/XMLDecoder)? O! :)
UsuńA :-) Nie widzę wyższości w żadnej z tych metod, obie są do ciut innych rzeczy. XMLEncoder/XMLDecoder są świetnie w przypadku JavaBean'ów, serializacja/deserializacja jest dobra jeśli chodzi o bardziej złożone obiekty.
UsuńXMLEncoder/XMLDecoder dotykają się tylko do publicznych właściwości dostępnych prze zestaw setterów i getterów, czyli jak coś nie jest przez nie dostępne to się nie zachowa. W przypadku serializacji się zwykle zachowa (jak jest prymitywem lub czymś co implementuje Serializable i sam obiekt to też implementuje ;-)).
Obie rzeczy fajne do pewnych zastosowań.
XMLEncoder/XMLDecoder się wywala na sytuacji gdy się klasa zmienia. W przypadku Serializable jest tam ta ukryta zmienna która mówi o wersji klasy (w razie nie zrobimy jej sami, Java ją doda) i jak się nie zgadzają to krzyczy. Tutaj po prostu się wywala dekodowanie jak w pliku jest więcej danych niż powinno być. Z odczytem jest lepiej, bo jeśli czytamy plik z mniejszą ilością pól niż ma nasza klasa to zostaną wartości domyślne dla tych pól.
Oba rozwiązania są do ciut innych rzeczy ;-)
Dzięki! O to mi chodziło :)
Usuń