poniedziałek, maja 10, 2010

Refleksje i serializacja w Java'ie - podstawy i obalanie mitów ;-)

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 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 ;-)

14 komentarzy:

  1. 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ń
  2. A masz jakieś sugestie co bym mogło się przydać opisać? ;-)

    OdpowiedzUsuń
  3. to chyba pierwszy blog który dodałem do ulubionych :D

    OdpowiedzUsuń
  4. Ś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.
    "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

    OdpowiedzUsuń
  5. Adak - Twoje 3 grosze są bardzo pomocne :) Dzięki!

    OdpowiedzUsuń
  6. Uwaga!
    W artykule jest błąd. Pola statyczne nie są ignorowane. Przynajmniej takie wnioski wyciągnąłem z krótkiego testu.

    OdpowiedzUsuń
  7. @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.
    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.

    OdpowiedzUsuń
  8. Tak, wiem. Pomyłka w teście, który sobie zrobiłem :)

    OdpowiedzUsuń
  9. 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ń
  10. 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ń
    Odpowiedzi
    1. Szczerze nie rozumiem pytania :-)
      Czy chodzi Ci o serializację/deserializację do/z XMLa?

      Usuń
    2. W czym widzisz wyższość serializacji/deserializacji nad zapisywaniem stanów obiektów do XMLa przy (XMLEncoder/XMLDecoder)? O! :)

      Usuń
    3. 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.
      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 ;-)

      Usuń
    4. Dzięki! O to mi chodziło :)

      Usuń