Dziś postanowiłem wrócić do tematu ;-)
Zacznę od tego, że Serializable był rzeczą, która mnie zachwyciła w Java'ie.
Do teraz mnie zachwyca, chociaż już nie tak bardzo ;-)
Czym jest serializacja?
To zamiana obiektu w strumień danych, dzięki czemu można obiekty zapisywać "na później" albo przesyłać.
Po drugiej stronie procesu jest deserializacja, czyli "zbudowanie" obiektu na podstawie tego co wcześniej zapisaliśmy.
Jakie obiekty są serializowalne?
Żeby obiekt był serializowalny w Java'ie MUSI implementować Serializable.
Sam interfejs Serializable jest interfejsem znacznikowym, nie wprowadza wymogu implementacji żadnych metod, a sygnalizuje tylko JVM, że dany obiekt obiecuje, że da się zserializować.
To jest tylko obietnica.
Kompilator w compile time nie jest w stanie stwierdzić czy to jest prawda.
JVM dopiero w runtime próbując dokonać serializacji, czy też deserializacji sprawdza czy jest to możliwe.
Jest to istotne o tyle, że fakt posiadania wśród przodków klasy, które implementuje Serializable i jest serializowalna nie oznacza wcale, że nasza klasa ładnie się zserializuje.
Samo implementowanie Serializable też nie znaczy, że jesteśmy z automatu serializowalni.
By to była prawda musimy zapewnić tą serializowalność.
Pola w nasze klasie, jak i superklasach muszą być serializowalne, oznaczone jako transient albo musimy dostarczyć mechanizmu serializacji.
Wszystkie pola niestatyczne i nie oznaczone jako transient są serializowane, niezależnie od ich widoczności.
Do tego to, że obiekt dało się zserializować nie oznacza od razu, że uda się go zdeserializować.
I nie i tak ;-)
Ogólnie nie, jeśli klasa implementuje Serializable to jej konstruktor nie będzie wołany.
Ale, jeśli któryś z przodków po drodze nie implementuje Serializable i nie ma konstruktora domyślnego to mamy problem ;-)
Tutaj warto pamiętać, że jeśli nie napiszemy żadnego konstruktora dla naszej klasy, to Java sama doda bezparametrowy konstruktor domyślny.
Do tego gdy piszemy nasz konstruktor i nie zawołamy w naszym konstruktorze konstruktora klasy bazowej to Java dopisze nam super() jako pierwszą linię naszego konstruktora, czyli wywoła konstruktor domyślny superklasy.
Jeśli nie ma takiego konstruktora to kod nam się nie skompiluje, no chyba że sami zrobimy super z parametrami do jednego z istniejących konstruktorów.
W trakcie deserializacji JVM używa więc konstruktorów dla tych klas które są w hierarchii przed pierwszą klasą oznaczoną jako taka, która implementuje Serializable.
Jeśli ta klasa nie ma konstruktora domyślnego, a nasza klasa ma wywołanie któregoś z niedomyślnych (czyli kod się kompiluje) to choć mamy kompilowalny kod to deserializacja będzie niemożliwa.
Stąd rule of thumb jest, jeśli już musisz używać serializacji/deserializacji to upewnij się, że klasa bazowa na której budujesz swój obiekt implementujący Serializable ma konstruktor domyślny ;-)
Jak zserializować obiekt?
To jest akurat proste, trzeba skorzystać z ObjetOutputStream, tutaj przykład jak to wygląda gdy chcemy nasz obiekt zapisać do pliku:
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("toster.dat"));
oos.writeObject(obj);
oos.close();
To zapisze nam obiekt do pliku "toster.dat" (oczywiście trzeba jeszcze obsłużyć wyjątki, które mogą stąd polecieć).
To wszystko przy założeniu, że obj, które próbujemy zapisać jest serializowalne.
OK, czyli jak sprawić by obiekt był serializowalny?
Jak już było wyżej, by obiekt był serializowalny wymagania są takie:
- musi implementować Serializable (lub jedna z jego superklas musi),
- wszystkie pola muszą być albo:
- serializowalne (czyli implementować Serializable),
- byś typami prostymi,
- być oznaczone jako transient
- by je zapisać trzeba dostarczyć własny mechanizm serializacji, lub pole takie będzie zignorowane.
Pola, które są typów prostych, lub takich które implementują Serializable zostaną zserializowane przez wbudowane w JVM mechanizmy, pola transient zostaną przez te mechanizmy zignorowane.
Jak zserializować dane z pól oznaczonych jako transient?
Da się ;-)
W takim przypadku trzeba dostarczyć własny mechanizm, który to mechanizm opiera się o 3 metody:
W trakcie serializacji ważna jest pierwsza metoda, dwie pozostałe są istotne w trakcie deserializacji. private void writeObject(java.io.ObjectOutputStream out) throws IOException; private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException;
private void readObjectNoData() throws ObjectStreamException;
Najpierw wołamy defaultWriteObject(), która to metoda zapisze za nas wszystkie pola niestatyczne i nietransient.
Oczywiście nie ma obowiązku wołania tej metody ;-)
Dalej dajemy kod, który zapisuje wartości które będziemy umieli później wykorzystać do zbudowania obiektu czy obiektów oznaczonych jako transient.
Jak zdeserializować obiekt?
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("toster.dat")); OurType object = (OurType)ois.readObject();
ois.close();
Warto zwrócić uwagę na to rzutowanie w 2 linii, które jest ważne o tyle, że readObject() zwraca Object, który musimy zrzutować na to co wczytujemy.
Oczywiście wypadałoby wcześniej sprawdzić czy to co wczytujemy jest instancją naszego typu i dopiero wtedy rzutować.
Mamy też dla pól oznaczonych jako transient metodę readObject(), która najpierw przy pomocy defaultReadObject(), wczytuje standardowo zapisane pola i po tym implementujemy mechanizm wczytujący pola transient.
W przypadku methody readObjectNoData() napiszę to co napisałem 9 lat temu ;-)
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ć :-(
Oczywiście mechanizm z writeObject/readObject można wykorzystać także do napisania własnego mechanizmu serializacji/deserializacji, nie ma przymusu używania tych wbudowanych w Java'ę.
Serializacja w Java'ie pozwala jeszcze na użycie metod writeReplace() i readResolve(), które służą temu by zapisać alternatywne obiekty.
Powiedzmy, że nasz obiekt jest serializowalny, ale nie chcemy go zapisywać całego, potrafimy zapisać go w "lepszy" sposób, wtedy implementujemy writeReplace() które zwróci inny obiekt, który zostanie zapisany przez domyślny mechanizm serializacji.
readResolve() zostanie użyte w trakcie deserializacji.
Przyznam, że nigdy nie musiałem używać tych 2 metod.
A co z wersjonowaniem?
No właśnie...
Nie jest dobrze z wersjonowaniem.
Obiekt implementujący Serializable powinien mieć statyczne finalne pole typu long o nazwie serialVersionUID, które mówi o wersji obiektu.
W trakcie serializacji wartość tego pola jest zapisywana, a w trakcie deserializacji wartość tego pola jest porównywalna z tą w klasie deserializowanego obiektu i jeśli się nie zgadzają JVM zgłasza wyjątek.
Jeśli nie stworzymy sami tego pola to kompilator zrobi to za nas. Ale jeśli zmienimy cokolwiek w naszej klasie, dodamy pole, usuniemy, albo nawet po prostu zrobimy dowolną edycję w pliku to kolejna skompilowana klasa z naszego pliku będzie miała inną wartość serialVersionUID....
Czyli chociaż mechanizm deserializacji powinien być w stanie odczytać nasz plik to nie będzie umiał.
Nawet nasza własna implementacja readObject nas tu nie uratuje....
Czy powinno się używać serializacji w Java'ie?
Nie do końca ;-)
Po pierwsze problem z wersjonowaniem jest poważny i mocno ogranicza możliwość pracy z Serializable.
Po drugie i chyba ważniejsze ;-) Oracle planuje wyłączenie serializacji w kolejnych wersjach Java'y.
To przez to, że serializacja i deserializacja są źródłem największej ilości błędów bezpieczeństwa w Java'ie.
Wszystko dzięki temu, że cały proces deserializacji to nic innego jak interpreter, a interpretery w samej swej naturze są niebezpieczne.
Stąd jeśli zdecydujesz się na używanie serializacji to z jednej strony ryzykujesz, że któraś z przyszłych wersji Java'y wyłączy ten mechanizm, ryzykujesz też otwarcie powierzchni do ataku swojej aplikacji.
Istnieją też mechanizmy alternatywne, które lepiej nadają się do serializacji i deserializacji obiektów.
Ale to już temat na kolejny wpis ;-)
Podobne postybeta
Refleksje i serializacja w Java'ie - podstawy i obalanie mitów ;-)
Wymiana obiektów między PC a Androidem... - użyj serializacji Luke ;-)
Lenistwo w działaniu, "piklujemy" Androida ;-)
Czemu trzeba pomagać Jacksonowi? ;-)
Konstruktory
Brak komentarzy:
Prześlij komentarz