PO Serializacja: Różnice pomiędzy wersjami
Linia 201: | Linia 201: | ||
Ponieważ ''java.io.Externalizable'' jest podinterfejsem ''java.io.Serializable'', implementujące go obiekty można przekazywać jako parametr metody ''writeObject()'' oraz mogą występować w serializowanym grafie obiektów. Za serializację odpowiada metoda ''writeExternal()'', a za deserializację ''readExternal()''. W wypadku klas implementujących ''Externalizable'' nie ma możliwości skorzystania z domyślnego mechanizmu serializacji. Ponadto w odróżnieniu od klas implementujących jedynie ''java.io.Serializable'' w trakcie deserializacji dochodzi do wywołania bezparametrowego konstruktora. Jeżeli klasa takiego nie posiada (musi mieć dostęp publiczny), zgłaszany jest wyjątek ''java.io.InvalidClassException''. | Ponieważ ''java.io.Externalizable'' jest podinterfejsem ''java.io.Serializable'', implementujące go obiekty można przekazywać jako parametr metody ''writeObject()'' oraz mogą występować w serializowanym grafie obiektów. Za serializację odpowiada metoda ''writeExternal()'', a za deserializację ''readExternal()''. W wypadku klas implementujących ''Externalizable'' nie ma możliwości skorzystania z domyślnego mechanizmu serializacji. Ponadto w odróżnieniu od klas implementujących jedynie ''java.io.Serializable'' w trakcie deserializacji dochodzi do wywołania bezparametrowego konstruktora. Jeżeli klasa takiego nie posiada (musi mieć dostęp publiczny), zgłaszany jest wyjątek ''java.io.InvalidClassException''. | ||
Przy podejmowaniu decyzji, czy domyślny mechanizm serializacji zastąpić dodając metody ''writeObject()'' i ''readObject()'', czy implementując interfejs ''java.io.Externalizable'' warto wziąć pod uwagę kwestie związane z przesłanianiem metod i korzystaniem z metod nadklasy przez podklasę. ''writeObject()'' i ''readObject()'' są prywatne, a ''writeExternal()'' i ''readExternal()'' muszą być publiczne ponieważ są wymuszone przez interfejs. W obu przypadkach wiążą się z tym pewne wady i zalety. | Przy podejmowaniu decyzji, czy domyślny mechanizm serializacji zastąpić dodając metody ''writeObject()'' i ''readObject()'', czy implementując interfejs ''java.io.Externalizable'', warto wziąć pod uwagę kwestie związane z przesłanianiem metod i korzystaniem z metod nadklasy przez podklasę. ''writeObject()'' i ''readObject()'' są prywatne, a ''writeExternal()'' i ''readExternal()'' muszą być publiczne ponieważ są wymuszone przez interfejs. W obu przypadkach wiążą się z tym pewne wady i zalety. | ||
== Dostępność definicji klasy podczas deserializacji == | == Dostępność definicji klasy podczas deserializacji == |
Wersja z 13:17, 13 wrz 2006
Wprowadzenie
Omówione na poprzednim wykładzie rodzaje strumieni udostępniają metody do zapisywania/odczytywania jedynie wartości typów podstawowych, tablic ze znakami oraz napisów String. Przy ich pomocy można przygotować kod wypisujący i odczytujący dowolne obiekty. Często jest to pracochłonne zadanie, gdyż atrybutami obiektów mogą być przecież inne obiekty. Java, tak jak większość języków obiektowych, zawiera udogodnienia pozwalające przesyłać przez strumienie obiekty praktycznie bez żadnej dodatkowej pracy. Przekształcanie obiektów na postać binarną lub znakową (np. XML) w sposób, który umożliwia ich późniejsze odtworzenie nazywa się serializacją (ang. serialization), a proces odwrotny określa się jako deserializację (ang. deserialization) <ref name="PO_Nazwy_Serializacji">Równocześnie funkcjonują dwa alternatywne terminy angielskie marshalling/unmarshalling oraz deflating/inflating.</ref>.
Zastosowania
Lekka trwałość
W większości aplikacji zachodzi potrzeba utrwalania danych. Może być na przykład spowodowana koniecznością zachowania tych danych pomiędzy uruchomieniami aplikacji, nie mieszczeniem się wszystkich danych na raz w pamięci operacyjnej lub chęcią zabezpieczenia się przed ich utratą. Do zapewnienia trwałości zazwyczaj stosuje się bazy danych, którym poświęcony jest inny cykl wykładów. W sytuacjach, gdy zapewniana przez bazy danych możliwość łatwego wyszukiwania informacji nie jest niezbędna (np. zapisywanie dokumentów do pliku – za organizację plików w strukturze katalogów i ich wyszukiwanie odpowiada użytkownik), serializacja może okazać się wystarczająca.
Programowanie rozproszone
W obiektowych aplikacjach rozproszonych najwygodniej przez sieć przesyłać całe obiekty. Serializacja może być używana do przesyłania obiektów przez strumienie oparte na gniazdach sieciowych oraz do przekazywania parametrów i wartości zwrotnych przy zdalnym wywoływaniu metod. Serializacja leży u podstaw Javowego mechanizmu zdalnego wywoływania metod Remote Method Invocetion (RMI), który jest wykorzystywany w wielu technologiach np. Enterprise Java Beans.
Klasy ObjectOutputStream, ObjectInputStream oraz interfejs Serializable
Do serializacji obiektów na postać binarną służą klasy ObjectOutputStream i ObjectInputStream, które posiadają odpowiednio metody writeObject(Object) oraz Object readObject(). Aby obiekt był poddany automatycznej serializacji musi implementować interfejs java.io.Serializable. Nie posiada on żadnych metod i pełni jedynie rolę znacznika. Większość klas ze standardowych bibliotek Javy, m.in. wszystkie klasy opakowujące, kolekcje oraz napisy String implementuje ten interfejs. Poniższy przykład pokazuje, że serializacji poddawany jest cały graf obiektów – obiekt przekazywany jako parametr metody writeObject() oraz wszystkie obiekty dostępne bezpośrednio lub pośrednio przez zawarte w nim referencje. Taki graf nazywany jest często domknięciem (ang. closure). Pozostałe obiekty należące do domknięcia również powinny implementować interfejs java.io.Serializable.
import java.io.*; class Osoba implements Serializable { String nazwisko; String imię; Adres adresZameldowania; Osoba(String nazwisko, String imię, Adres adresZameldowania) { this.nazwisko = nazwisko; this.imię = imię; this.adresZameldowania = adresZameldowania; System.out.println("wywołanie konstruktora klasy Osoba"); } public String toString() { String adrPamięć = super.toString(); return adrPamięć+"(" + nazwisko + ", " + imię + ", " + adresZameldowania + ")"; } } class Adres implements Serializable { String miasto; String ulica; String nrDomu; String nrLokalu; Adres(String miasto, String ulica, String nrDomu, String nrLokalu) { this.miasto = miasto; this.ulica = ulica; this.nrDomu = nrDomu; this.nrLokalu = nrLokalu; System.out.println("wywołanie konstruktora klasy Adres"); } public String toString() { String adrPamięć = super.toString(); return adrPamięć + "(" + miasto + ", " + ulica + ", " + nrDomu + ", " + nrLokalu + ")"; } } public class TestSerializacji { public static void main(String[] args) throws Exception { Adres alternatywy4 = new Adres("Warszawa", "Alternatywy", "4", "9"); Osoba kotek = new Osoba("Kotek", "Zygmunt", alternatywy4); System.out.println(kotek); // wersja dla Linuxa String nazwaPliku = "/tmp/lista.ser"; // wersja dla Windows //String nazwaPliku = "c:\\lista.ser"; ObjectOutputStream out = new ObjectOutputStream( new BufferedOutputStream( new FileOutputStream(nazwaPliku))); out.writeObject("Lista lokatorów"); out.writeObject(kotek); out.close(); ObjectInputStream in = new ObjectInputStream( new BufferedInputStream( new FileInputStream(nazwaPliku))); String nagłówek = (String) in.readObject(); kotek = (Osoba) in.readObject(); in.close(); System.out.println(kotek); } }
Podczas deserializacji nie dochodzi do wywołania żadnych konstruktorów, mimo że tworzone są nowe egzemplarze obiektów. Potwierdza to wyjście powyższego programu:
wywołanie konstruktora klasy Adres wywołanie konstruktora klasy Osoba Osoba@1372a1a(Kotek, Zygmunt, Adres@ad3ba4(Warszawa, Alternatywy, 4, 9)) Osoba@1c39a2d(Kotek, Zygmunt, Adres@bf2d5e(Warszawa, Alternatywy, 4, 9))
Kilka referencji wskazujących ten sam obiekt
Algorytm użyty do serializacji grafu obiektów jest przygotowany na występowanie w tym grafie cykli. Jeżeli przed serializacją jakiś obiekt był przypisany na kilka różnych referencji, po deserializacji nadal będzie na te referencje przypisany jeden obiekt. Co więcej własność ta jest zachowana nie tylko przy serializacji jednego grafu obiektów, ale w sumie dla wszystkich grafów serializowanych do jednego strumienia. To znaczy, że jeżeli w kilku serializowanych do tego samego strumienia grafach obiektów występował wspólny obiekt, to po deserializacji grafy nadal będą posiadały część wspólną. Poniższy przykład pokazuje serializację do jednego strumienia dwóch osób posiadających ten sam adres.
Adres alternatywy4 = new Adres("Warszawa", "Alternatywy", "4", "9"); Osoba kotek = new Osoba("Kotek", "Zygmunt", alternatywy4); Osoba kołek= new Osoba("Kołek", "Zdzisław", alternatywy4); System.out.println(kotek); System.out.println(kołek); // wersja dla Linuxa String nazwaPliku = "/tmp/lista.ser"; // wersja dla Windows //String nazwaPliku = "c:\\lista.ser"; ObjectOutputStream out = new ObjectOutputStream( new BufferedOutputStream( new FileOutputStream(nazwaPliku))); out.writeObject("Lista lokatorów"); out.writeObject(kotek); out.writeObject(kołek); out.close(); ObjectInputStream in = new ObjectInputStream( new BufferedInputStream( new FileInputStream(nazwaPliku))); String nagłówek = (String) in.readObject(); kotek = (Osoba) in.readObject(); kołek = (Osoba) in.readObject(); in.close(); System.out.println(kotek); System.out.println(kołek);
Po deserializacji adresy obu osób nie tylko będą posiadały te same atrybuty, ale nadal będą tym samym obiektem.
wywołanie konstruktora klasy Adres wywołanie konstruktora klasy Osoba wywołanie konstruktora klasy Osoba Osoba@1372a1a(Kotek, Zygmunt, Adres@ad3ba4(Warszawa, Alternatywy, 4, 9)) Osoba@126b249(Kołek, Zdzisław, Adres@ad3ba4(Warszawa, Alternatywy, 4, 9)) Osoba@df8ff1(Kotek, Zygmunt, Adres@1632c2d(Warszawa, Alternatywy, 4, 9)) Osoba@1e97676(Kołek, Zdzisław, Adres@1632c2d(Warszawa, Alternatywy, 4, 9))
Kontrolowanie serializacji
Domyślnie serializowane są wszystkie niestatyczne składowe każdego obiektu, nawet jeżeli są oznaczone jako prywatne<ref name="PO_ser_dostęp_do_pry_składowych">Dostęp do składowych prywatnych obiektu jest możliwy dzięki mechanizmowi odzwierciedleń (ang. reflection).</ref>. W niektórych sytuacjach nie jest to wskazane. Po pierwsze, obiekty mogą zawierać dane poufne, np. hasła, których nie chcemy wysyłać do strumienia. Po drugie, serializaowanie niektórych obiektów nie ma sensu, np. strumieni lub wątków. Po trzecie, nie wszystkie obiekty implementują interfejs java.io.Serializable i nie ma możliwości tego zmienić, a przy próbie serializacji grafu obiektów, który zawiera obiekt nieimplementujący tego interfejsu zgłaszany jest wyjątek java.io.NotSerializableException. Są dwa sposoby żeby sobie z tym poradzić: wskazanie atrybutów, które powinny być pomijane przez domyślny mechanizm serializacji oraz rezygnacja z domyślnego mechanizmu serializacji i podanie kodu, które ma go zastąpić.
Słowo kluczowe transient
Atrybuty które mają być wyłączone z domyślnego mechanizmu serializacji należy oznaczyć przy pomocy modyfikatora transient. Deserializacja pozostawia na takich atrybutach wartość domyślną dla danego typu, np. dla typów obiektowych jest to null. W poniższym przykładzie modyfikatorem transient oznaczono atrybut adres klasy Osoba.
class Osoba implements Serializable { String nazwisko; String imię; transient Adres adresZameldowania; Osoba(String nazwisko, String imię, Adres adresZameldowania) { this.nazwisko = nazwisko; this.imię = imię; this.adresZameldowania = adresZameldowania; System.out.println("wywołanie konstruktora klasy Osoba"); } public String toString() { String adrPamięć = super.toString(); return adrPamięć+"(" + nazwisko + ", " + imię + ", " + adresZameldowania + ")"; } }
Po deserializacji w każdym obiekcie atrybut adres będzie miał domyślną dla typu obiektowego wartość null.
wywołanie konstruktora klasy Adres wywołanie konstruktora klasy Osoba wywołanie konstruktora klasy Osoba Osoba1@1372a1a(Kotek, Zygmunt, Adres@ad3ba4(Warszawa, Alternatywy, 4, 9)) Osoba1@126b249(Kołek, Zdzisław, Adres@ad3ba4(Warszawa, Alternatywy, 4, 9)) Osoba1@750159(Kotek, Zygmunt, null) Osoba1@1abab88(Kołek, Zdzisław, null)
Metody writeObject() i readObject()
Można zupełnie zrezygnować z domyślnego mechanizmu serializacji i podać kod go zastępujący. Jeżeli klasa definiuje metody:
- private void writeObject(ObjectOutputStream oos) throws IOException;
- private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException;
będą one używane do serializacji i deserializacji jej obiektów. To nie jest pomyłka, że metody są oznaczone jako prywatne<ref name="PO_ser_dostęp_do_pry_składowych">Dostęp do składowych prywatnych obiektu jest możliwy dzięki mechanizmowi odzwierciedleń (ang. reflection).</ref>! Jeżeli nie zależy nam na zupełnym zastąpieniu, ale na rozszerzeniu domyślnego mechanizmu serializacji, w metodach writeObject() i readObject() można wywołać na przekazanym jako parametr strumieniu odpowiednio defaultWriteObject() oraz defaultReadObject(). W poniższym przykładzie domyślny mechanizm został rozszerzony o serializację atrybutu adres, który normalnie nie był serializowany ze względu na oznaczenie go modyfikatorem transient.
class Osoba implements Serializable { String nazwisko; String imię; transient Adres adresZameldowania; private void writeObject(ObjectOutputStream oos) throws IOException { System.out.println("writeObject()"); oos.defaultWriteObject(); oos.writeObject(adresZameldowania); } private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { System.out.println("readObject()"); ois.defaultReadObject(); adresZameldowania = (Adres) ois.readObject(); } Osoba(String nazwisko, String imię, Adres adresZameldowania) { this.nazwisko = nazwisko; this.imię = imię; this.adresZameldowania = adresZameldowania; System.out.println("wywołanie konstruktora klasy Osoba"); } public String toString() { String adrPamięć = super.toString(); return adrPamięć+"(" + nazwisko + ", " + imię + ", " + adresZameldowania + ")"; } }
Interfejs Externalizable
Istnieje jeszcze jeden sposób zastąpienia domyślnego mechanizmu serializacji. Klasa może implementować interfejs java.io.Externalizable i definiować wymagane przez niego metody
- void writeExternal(ObjectOutput out) throws IOException;
- void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
Ponieważ java.io.Externalizable jest podinterfejsem java.io.Serializable, implementujące go obiekty można przekazywać jako parametr metody writeObject() oraz mogą występować w serializowanym grafie obiektów. Za serializację odpowiada metoda writeExternal(), a za deserializację readExternal(). W wypadku klas implementujących Externalizable nie ma możliwości skorzystania z domyślnego mechanizmu serializacji. Ponadto w odróżnieniu od klas implementujących jedynie java.io.Serializable w trakcie deserializacji dochodzi do wywołania bezparametrowego konstruktora. Jeżeli klasa takiego nie posiada (musi mieć dostęp publiczny), zgłaszany jest wyjątek java.io.InvalidClassException.
Przy podejmowaniu decyzji, czy domyślny mechanizm serializacji zastąpić dodając metody writeObject() i readObject(), czy implementując interfejs java.io.Externalizable, warto wziąć pod uwagę kwestie związane z przesłanianiem metod i korzystaniem z metod nadklasy przez podklasę. writeObject() i readObject() są prywatne, a writeExternal() i readExternal() muszą być publiczne ponieważ są wymuszone przez interfejs. W obu przypadkach wiążą się z tym pewne wady i zalety.
Dostępność definicji klasy podczas deserializacji
Aby deserializacja obiektu się powiodła maszyna wirtualna, na której jest podejmowana taka próba musi mieć dostęp do definicji klasy. Można to sprawdzić wykonując poniższy przykład:
import java.io.*; class KlasaONiedostępnejDefinicji implements Serializable {} public class TestWersjonowania { public static void main(String[] args) throws Exception { // wersja dla Linuxa String nazwaPliku = "/tmp/test.ser"; // wersja dla Windows //String nazwaPliku = "c:\\test.ser"; ObjectOutputStream oos = new ObjectOutputStream( new BufferedOutputStream( new FileOutputStream(nazwaPliku))); oos.writeObject(new KlasaONiedostępnejDefinicji()); oos.close(); } }
Spowoduje to serializację obiektu klasy KlasaONiedostępnejDefinicji do pliku test.ser. Jeżeli podczas próby deserializacji klasa KlasaONiedostępnejDefinicji nie będzie dostępna dla maszyny wirtualnej (poprzez zmienną CLASSHPATH), zostanie zgłoszony wyjątek java.lang.ClassNotFoundException. Można się o tym przekonać uruchamiając poniższą klasę:
import java.io.*; public class PróbaOdczytu { public static void main(String[] args) throws Exception { ObjectInputStream ois = null; try { // wersja dla Linuxa String nazwaPliku = "/tmp/test.ser"; // wersja dla Windows //String nazwaPliku = "c:\\test.ser"; ois = new ObjectInputStream( new BufferedInputStream( new FileInputStream(nazwaPliku))); Object obj = ois.readObject(); } catch (Exception e) { System.out.println(e); } finally { try { ois.close(); } catch (Exception e) { System.out.println("Nie udało się zamknąć strumienia"); } } } }
Podglądając plik test.ser można się przekonać, że nazwa serializowanej klasy jest wypisywana do strumienia. Dzieje się tak nawet jeżeli zastąpiliśmy domyślny mechanizm serializacji dodając prywatną metodę writeObject(ObjectOutpuStream) lub implementując interfejs Externalizable. Dzięki temu maszyna wirtualna wie obiekt jakiej klasy utworzyć podczas deserializacji.
Wersjonowanie
Dociekliwe osoby już pewnie od jakiegoś czasu zastanawiają się, co jeżeli po serializacji definicja klasy ulegał zmianie i podejmowana jest próba jego deserializacji. Taka sytuacja jest dość prawdopodobna. Obiekt mógł np. być zserializowany do pliku dyskowego i przechowywany w ten sposób przez dłuższy czas lub mógł zostać wysłany przez sieć do zdalnej maszyny wirtualnej, na której jest uruchomiona inna wersja aplikacji. Jeżeli obiekt implementuje interfejs Externalizable programista sam odpowiada za poprawną obsługę wersjonowania. Najlepiej w metodzie writeExternal() wypisywać razem z danymi informacje o wersji klasy i na ich podstawie w readExternal() podjąć odpowiednie działania, np. ustawić domyślne wartości dla atrybutów, których nie zawierał zserializowany obiekt. Jeżeli natomiast klasa implementuje interfejs Serializable (ale nie Externalizable), to obsługa wersjonowania odbywa się automatycznie. Porównywanie klas wykonywane jest przy pomocy wartości typu long wyliczanej na podstawie:
- nazwy klasy i użytych przy jej definicji modyfikatorów,
- nazw wszystkich interfejsów implementowanych przez klasę,
- opisu wszystkich metod i konstruktorów oprócz metod i konstruktorów prywatnych,
- opisu wszystkich atrybutów poza atrybutami prywatnymi, statycznymi i oznaczonymi jako transient.
Podczas serializacji taki unikatowy identyfikator klasy jest generowany i wypisywany do strumienia, a podczas deserializacji jest porównywany z identyfikatorem klasy dostępnej dla maszyny wirtualnej. Jeżeli identyfikatory nie są równe deserializacja jest przerywana i zgłaszany jest wyjątek java.io.InvalidClassException. Takie domyślne zachowanie cechuje duża ostrożność. Można je zmienić samemu podając oznaczenie wersji klasy. Służy do tego atrybut static final long serialVersionUID potocznie określany jako suid. Poniższa klasa ma ustawiony numer wersji na 2L
class KlasaOZmieniającejSięDefinicji implements Serializable { static final long serialVersionUID = 2L; String staryAtrybut = "jakaś wartość"; }
Mimo usunięcia oraz dodania atrybutu pozostaje nadal kompatylina z klasą podaną poniżej
class KlasaOZmieniającejSięDefinicji implements Serializable { static final long serialVersionUID = 2L; String nowyAtrybut = "deserializacja nie przypisze tej wartości"; }
Zapisana w strumieniu wartość usuniętego atrybutu zostanie zignorowana, a nowy atrybut będzie miał wartość domyślną dla swojego typu. Oznacza to że podczas deserializacji nie zostanie nawet wyliczone wyrażenie inicjalizujące.
Odczytywanie suid istniejącej klasy
Jeżeli mamy do czynienia z istniejącą już klasą, w której nie podano atrybutu serialVersionUID, a chcemy zachować z nią kompatybilność, domyślnie wygenerowany dla niej suid można odczytać przy pomocy narzędzia serialver. Wchodzi ono w skład JDK i powinno być dostępne tak samo jak java.
Serializacja do postaci XML
Przypisy
<references/>