Na pierwszy rzut oka serializacja wydaje się trywialnym procesem. Naprawdę, co może być prostszego? Zadeklarowano klasę do implementacji interfejsu
java.io.Serializable
- i tyle. Możesz serializować klasę bez problemów. Teoretycznie jest to prawdą. W praktyce istnieje wiele subtelności. Są one związane z wydajnością, deserializacją i bezpieczeństwem klas. I z wieloma innymi aspektami. Takie subtelności zostaną omówione. Artykuł ten można podzielić na następujące części:
- Subtelności mechanizmów
- Dlaczego jest to potrzebne?
Externalizable
- Wydajność
- ale z drugiej strony
- Ochrona danych
- Serializacja obiektów
Singleton
Subtelności mechanizmów
Na początek szybkie pytanie. Na ile sposobów można serializować obiekt? Praktyka pokazuje, że ponad 90% programistów odpowiada na to pytanie w przybliżeniu w ten sam sposób (aż do sformułowania) - jest tylko jeden sposób. Tymczasem jest ich dwóch. Nie każdy pamięta ten drugi, nie mówiąc już o powiedzeniu czegoś zrozumiałego na temat jego cech. Jakie więc są te metody? Każdy pamięta pierwszą. Jest to wspomniana już implementacjajava.io.Serializable
i nie wymaga żadnego wysiłku. Druga metoda to także implementacja interfejsu, ale inna: java.io.Externalizable
. W przeciwieństwie do java.io.Serializable
tego zawiera dwie metody, które należy wdrożyć - writeExternal(ObjectOutput)
i readExternal(ObjectInput)
. Metody te zawierają logikę serializacji/deserializacji. Komentarz.Serializable
W dalszej części będę czasami odnosił się do serializacji z implementacją w standardzie i implementacją Externalizable
jako rozszerzoną. Innykomentarz. Celowo nie poruszam teraz takich standardowych opcji kontroli serializacji jak definiowanie readObject
i writeObject
, ponieważ Myślę, że te metody są nieco błędne. Metody te nie są zdefiniowane w interfejsie Serializable
i w rzeczywistości służą do obejścia ograniczeń i uelastycznienia standardowej serializacji. Externalizable
Od samego początku są w nie wbudowane metody zapewniające elastyczność . Zadajmy jeszcze jedno pytanie. Jak faktycznie działa standardowa serializacja przy użyciu java.io.Serializable
? Działa to poprzez API Reflection. Te. klasa jest analizowana jako zestaw pól, z których każde jest zapisywane w strumieniu wyjściowym. Myślę, że jasne jest, że ta operacja nie jest optymalna pod względem wydajności. Ile dokładnie dowiemy się później. Istnieje jeszcze jedna zasadnicza różnica pomiędzy dwiema wspomnianymi metodami serializacji. Mianowicie w mechanizmie deserializacji. W przypadku użycia Serializable
deserializacja przebiega w następujący sposób: pamięć jest przydzielana obiektowi, po czym jego pola są wypełniane wartościami ze strumienia. Konstruktor obiektu nie jest wywoływany. Tutaj musimy rozważyć tę sytuację osobno. OK, nasza klasa jest serializowalna. A jego rodzic? Całkowicie opcjonalne! Co więcej, jeśli dziedziczysz klasę od Object
- rodzica zdecydowanie NIE można serializować. I chociaż Object
nie wiemy nic o polach, mogą one istnieć w naszych własnych klasach nadrzędnych. Co się z nimi stanie? Nie dostaną się do strumienia serializacji. Jakie wartości przyjmą po deserializacji? Spójrzmy na ten przykład:
package ru.skipy.tests.io;
import java.io.*;
/**
* ParentDeserializationTest
*
* @author Eugene Matyushkin aka Skipy
* @since 05.08.2010
*/
public class ParentDeserializationTest {
public static void main(String[] args){
try {
System.out.println("Creating...");
Child c = new Child(1);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
c.field = 10;
System.out.println("Serializing...");
oos.writeObject(c);
oos.flush();
baos.flush();
oos.close();
baos.close();
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
System.out.println("Deserializing...");
Child c1 = (Child)ois.readObject();
System.out.println("c1.i="+c1.getI());
System.out.println("c1.field="+c1.getField());
} catch (IOException ex){
ex.printStackTrace();
} catch (ClassNotFoundException ex){
ex.printStackTrace();
}
}
public static class Parent {
protected int field;
protected Parent(){
field = 5;
System.out.println("Parent::Constructor");
}
public int getField() {
return field;
}
}
public static class Child extends Parent implements Serializable{
protected int i;
public Child(int i){
this.i = i;
System.out.println("Child::Constructor");
}
public int getI() {
return i;
}
}
}
Jest to przejrzyste — mamy klasę nadrzędną, której nie można serializować, i klasę podrzędną, która nie podlega serializacji. I oto co się dzieje:
Creating...
Parent::Constructor
Child::Constructor
Serializing...
Deserializing...
Parent::Constructor
c1.i=1
c1.field=5
Oznacza to, że podczas deserializacji wywoływany jest konstruktor bez parametrów klasy nadrzędnej NIEmożliwej do serializacji . A jeśli nie ma takiego konstruktora, podczas deserializacji wystąpi błąd. Konstruktor obiektu podrzędnego, tego, który deserializujemy, nie jest wywoływany, jak powiedziano powyżej. Tak zachowują się standardowe mechanizmy, gdy są używane Serializable
. Podczas jego używania Externalizable
sytuacja jest inna. Najpierw wywoływany jest konstruktor bez parametrów, a następnie na utworzonym obiekcie wywoływana jest metoda readExternal, która tak naprawdę odczytuje wszystkie jego dane. Dlatego każda klasa implementująca interfejs Externalizable musi mieć konstruktor publiczny bez parametrów! Co więcej, ponieważ wszyscy potomkowie takiej klasy będą również brani pod uwagę przy implementacji interfejsu Externalizable
, muszą także mieć konstruktora bez parametrów! Idźmy dalej. Istnieje taki modyfikator pola jak transient
. Oznacza to, że pole to nie powinno być serializowane. Jednak, jak sam rozumiesz, ta instrukcja dotyczy tylko standardowego mechanizmu serializacji. Kiedy jest używany, Externalizable
nikt nie zadaje sobie trudu serializacji tego pola, a także odejmowania go. Jeżeli pole jest zadeklarowane jako przejściowe, to po deserializacji obiektu przyjmuje ono wartość domyślną. Kolejny dość subtelny punkt. W przypadku serializacji standardowej pola zawierające modyfikator static
nie są serializowane. W związku z tym po deserializacji pole to nie zmienia swojej wartości. Oczywiście podczas implementacji Externalizable
nikt nie zawraca sobie głowy serializacją i deserializacją tego pola, ale zdecydowanie odradzam tego robić, ponieważ może to prowadzić do subtelnych błędów. Pola z modyfikatorem final
są serializowane jak zwykłe pola. Z jednym wyjątkiem — nie można ich deserializować podczas korzystania z opcji Outsideizable. Ponieważ final-поля
trzeba je zainicjalizować w konstruktorze, a po tym nie będzie już możliwości zmiany wartości tego pola w readExternal. W związku z tym, jeśli chcesz serializować obiekt posiadający final
pole -, będziesz musiał użyć jedynie standardowej serializacji. Kolejna kwestia, o której wiele osób nie wie. Standardowa serializacja uwzględnia kolejność deklarowania pól w klasie. W każdym razie tak było we wcześniejszych wersjach, w wersji JVM 1.6 implementacji Oracle kolejność nie jest już istotna, ważny jest rodzaj i nazwa pola. Skład metod najprawdopodobniej będzie miał wpływ na standardowy mechanizm, pomimo faktu, że pola mogą generalnie pozostać takie same. Aby tego uniknąć, istnieje następujący mechanizm. Do każdej klasy implementującej interfejs Serializable
na etapie kompilacji dodawane jest jeszcze jedno pole -private static final long serialVersionUID
. To pole zawiera unikalny identyfikator wersji serializowanej klasy. Oblicza się go na podstawie zawartości klasy - pól, kolejności ich deklaracji, metod, kolejności ich deklaracji. Odpowiednio przy każdej zmianie klasy pole to zmieni swoją wartość. To pole jest zapisywane w strumieniu podczas serializacji klasy. Nawiasem mówiąc, jest to chyba jedyny znany mi przypadek, gdy static
pole - jest serializowane. Podczas deserializacji wartość tego pola jest porównywana z wartością klasy na maszynie wirtualnej. Jeśli wartości nie są zgodne, zgłaszany jest taki wyjątek:
java.io.InvalidClassException: test.ser2.ChildExt;
local class incompatible: stream classdesc serialVersionUID = 8218484765288926197,
local class serialVersionUID = 1465687698753363969
Istnieje jednak sposób, aby, jeśli nie ominąć, to oszukać tę kontrolę. Może to być przydatne, jeśli zestaw pól klas i ich kolejność są już zdefiniowane, ale metody klas mogą ulec zmianie. W tym przypadku serializacja nie jest zagrożona, jednak standardowy mechanizm nie pozwoli na deserializację danych przy użyciu kodu bajtowego zmodyfikowanej klasy. Ale, jak powiedziałem, można go oszukać. Mianowicie ręcznie zdefiniuj pole w klasie private static final long serialVersionUID
. W zasadzie wartość tego pola może być absolutnie dowolna. Niektórzy wolą ustawić ją jako równą dacie modyfikacji kodu. Niektórzy używają nawet 1L. Aby uzyskać wartość standardową (tę obliczoną wewnętrznie), możesz użyć narzędzia serialver zawartego w SDK. Po zdefiniowaniu w ten sposób wartość pola zostanie stała, dlatego deserializacja będzie zawsze dozwolona. Co więcej, w wersji 5.0 w dokumentacji pojawiły się mniej więcej następujące informacje: zdecydowanie zaleca się, aby wszystkie klasy nadające się do serializacji jawnie deklarowały to pole, ponieważ domyślne obliczenia są bardzo wrażliwe na szczegóły struktury klasy, która może się różnić w zależności od implementacji kompilatora, i tym samym spowodować nieoczekiwane InvalidClassException
konsekwencje deserializacji. Lepiej zadeklarować to pole jako private
, ponieważ odnosi się wyłącznie do klasy, w której jest zadeklarowany. Chociaż modyfikator nie jest określony w specyfikacji. Rozważmy teraz ten aspekt. Powiedzmy, że mamy następującą strukturę klas:
public class A{
public int iPublic;
protected int iProtected;
int iPackage;
private int iPrivate;
}
public class B extends A implements Serializable{}
Innymi słowy, mamy klasę odziedziczoną od rodzica, którego nie można serializować. Czy można serializować tę klasę i co jest do tego potrzebne? Co stanie się ze zmiennymi klasy nadrzędnej? Odpowiedź jest taka. Tak, B
możesz serializować instancję klasy. Co jest do tego potrzebne? Ale klasa musi A
mieć konstruktor bez parametrów, public
lub protected
. Następnie podczas deserializacji wszystkie zmienne klasy A
zostaną zainicjowane przy użyciu tego konstruktora. Zmienne klasy B
zostaną zainicjalizowane wartościami z serializowanego strumienia danych. Teoretycznie możliwe jest zdefiniowanie w klasie B
metod, o których mówiłem na początku - readObject
i writeObject
, - na początku których należy wykonać (des-)serializację zmiennych klasy B
poprzez in.defaultReadObject/out.defaultWriteObject
, a następnie (de-)serializację dostępnych zmiennych z klasy A
(w naszym przypadku są to iPublic
, iProtected
i iPackage
, jeśli B
znajduje się w tym samym pakiecie co A
). Jednak moim zdaniem lepiej jest w tym celu zastosować rozszerzoną serializację. Następną kwestią, którą chciałbym poruszyć, jest serializacja wielu obiektów. Załóżmy, że mamy następującą strukturę klas:
public class A implements Serializable{
private C c;
private B b;
public void setC(C c) {this.c = c;}
public void setB(B b) {this.b = b;}
public C getC() {return c;}
public B getB() {return b;}
}
public class B implements Serializable{
private C c;
public void setC(C c) {this.c = c;}
public C getC() {return c;}
}
public class C implements Serializable{
private A a;
private B b;
public void setA(A a) {this.a = a;}
public void setB(B b) {this.b = b;}
public B getB() {return b;}
public A getA() {return a;}
}
Co się stanie, jeśli serializujesz instancję klasy A
? Przeciągnie instancję klasy B
, która z kolei przeciągnie instancję C
mającą odwołanie do instancji A
, tej samej, od której wszystko się zaczęło. Błędne koło i nieskończona rekurencja? Na szczęście nie. Spójrzmy na następujący kod testowy:
// initiaizing
A a = new A();
B b = new B();
C c = new C();
// setting references
a.setB(b);
a.setC(c);
b.setC(c);
c.setA(a);
c.setB(b);
// serializing
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(a);
oos.writeObject(b);
oos.writeObject(c);
oos.flush();
oos.close();
// deserializing
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
A a1 = (A)ois.readObject();
B b1 = (B)ois.readObject();
C c1 = (C)ois.readObject();
// testing
System.out.println("a==a1: "+(a==a1));
System.out.println("b==b1: "+(b==b1));
System.out.println("c==c1: "+(c==c1));
System.out.println("a1.getB()==b1: "+(a1.getB()==b1));
System.out.println("a1.getC()==c1: "+(a1.getC()==c1));
System.out.println("b1.getC()==c1: "+(b1.getC()==c1));
System.out.println("c1.getA()==a1: "+(c1.getA()==a1));
System.out.println("c1.getB()==b1: "+(c1.getB()==b1));
Co my robimy? Tworzymy instancję klas A
i łączymy je ze sobą, a następnie serializujemy każdą z nich B
. C
Następnie deserializujemy je z powrotem i przeprowadzamy serię kontroli. Co się stanie w rezultacie:
a==a1: false
b==b1: false
c==c1: false
a1.getB()==b1: true
a1.getC()==c1: true
b1.getC()==c1: true
c1.getA()==a1: true
c1.getB()==b1: true
Czego więc możesz się dowiedzieć z tego testu? Pierwszy. Odniesienia do obiektów po deserializacji różnią się od odwołań przed nią. Innymi słowy, podczas serializacji/deserializacji obiekt został skopiowany. Ta metoda jest czasami używana do klonowania obiektów. Drugi wniosek jest bardziej znaczący. Podczas serializacji/deserializacji wielu obiektów, które mają odniesienia, odniesienia te pozostają ważne po deserializacji. Innymi słowy, jeśli przed serializacją wskazywały na jeden obiekt, to po deserializacji również wskażą jeden obiekt. Kolejny mały test, aby to potwierdzić:
B b = new B();
C c = new C();
b.setC(c);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(b);
oos.writeObject(c);
oos.writeObject(c);
oos.writeObject(c);
oos.flush();
oos.close();
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
B b1 = (B)ois.readObject();
C c1 = (C)ois.readObject();
C c2 = (C)ois.readObject();
C c3 = (C)ois.readObject();
System.out.println("b1.getC()==c1: "+(b1.getC()==c1));
System.out.println("c1==c2: "+(c1==c2));
System.out.println("c1==c3: "+(c1==c3));
Obiekt klasy B
zawiera odwołanie do obiektu klasy C
. Kiedy jest serializowany, b
jest serializowany wraz z instancją klasy С
, po czym ta sama instancja c jest serializowana trzykrotnie. Co się dzieje po deserializacji?
b1.getC()==c1: true
c1==c2: true
c1==c3: true
Jak widać, wszystkie cztery deserializowane obiekty w rzeczywistości reprezentują jeden obiekt - odniesienia do niego są równe. Dokładnie tak jak przed serializacją. Kolejna interesująca kwestia - co się stanie, jeśli jednocześnie wdrożymy Externalizable
i Serializable
? Jak w przypadku tego pytania – słoń kontra wieloryb – kto kogo pokona? przezwycięży Externalizable
. Mechanizm serializacji najpierw sprawdza jego obecność, a dopiero potem jego obecność, Serializable
więc jeśli klasa B, która implementuje Serializable
, dziedziczy po klasie A, która implementuje Externalizable
, pola klasy B nie będą serializowane. Ostatnią kwestią jest dziedziczenie. Podczas dziedziczenia z klasy, która implementuje Serializable
, nie trzeba podejmować żadnych dodatkowych działań. Serializacja obejmie również klasę podrzędną. Dziedzicząc z klasy, która implementuje Externalizable
, należy zastąpić metody readExternal i writeExternal klasy nadrzędnej. W przeciwnym razie pola klasy podrzędnej nie zostaną serializowane. W takim przypadku należy pamiętać o wywołaniu metod nadrzędnych, w przeciwnym razie pola nadrzędne nie zostaną serializowane. * * * Prawdopodobnie skończyliśmy ze szczegółami. Jest jednak jedna kwestia, której nie poruszyliśmy, a która ma charakter globalny. Mianowicie -
Dlaczego potrzebujesz eksternalizowalności?
Po co nam w ogóle zaawansowana serializacja? Odpowiedź jest prosta. Po pierwsze, daje znacznie większą elastyczność. Po drugie, często może zapewnić znaczne korzyści pod względem ilości serializowanych danych. Po trzecie, istnieje taki aspekt, jak wydajność, o którym porozmawiamy poniżej . Dzięki elastyczności wszystko wydaje się jasne. Rzeczywiście, możemy dowolnie kontrolować procesy serializacji i deserializacji, co uniezależnia nas od jakichkolwiek zmian w klasie (jak powiedziałem powyżej, zmiany w klasie mogą znacząco wpłynąć na deserializację). Dlatego chcę powiedzieć kilka słów o wzroście głośności. Powiedzmy, że mamy następującą klasę:public class DateAndTime{
private short year;
private byte month;
private byte day;
private byte hours;
private byte minutes;
private byte seconds;
}
Reszta jest nieważna. Pola mogłyby być typu int, ale to tylko wzmocniłoby efekt przykładu. Chociaż w rzeczywistości pola mogą być wpisywane int
ze względu na wydajność. W każdym razie sprawa jest jasna. Klasa reprezentuje datę i godzinę. Jest to dla nas interesujące przede wszystkim z punktu widzenia serializacji. Być może najłatwiejszym rozwiązaniem byłoby przechowywanie prostego znacznika czasu. Jest typu long, tj. po serializacji zajmie 8 bajtów. Dodatkowo podejście to wymaga metod konwersji komponentów na jedną wartość i odwrotnie, tj. – utrata produktywności. Zaletą takiego podejścia jest całkowicie szalona data mieszcząca się w 64 bitach. To ogromny margines bezpieczeństwa, najczęściej niepotrzebny w rzeczywistości. Klasa podana powyżej zajmie 2 + 5*1 = 7 bajtów. Plus koszty ogólne za zajęcia i 6 pól. Czy jest jakiś sposób na skompresowanie tych danych? Z pewnością. Sekundy i minuty mieszczą się w zakresie 0-59, tj. do ich przedstawienia wystarczy 6 bitów zamiast 8. Godziny – 0-23 (5 bitów), dni – 0-30 (5 bitów), miesiące – 0-11 (4 bity). Razem wszystko bez uwzględnienia roku - 26 bitów. Pozostało jeszcze 6 bitów do rozmiaru int. Teoretycznie w niektórych przypadkach może to wystarczyć na rok. Jeśli nie, dodanie kolejnego bajtu zwiększa rozmiar pola danych do 14 bitów, co daje zakres 0-16383. To jest więcej niż wystarczające w rzeczywistych zastosowaniach. Łącznie zmniejszyliśmy rozmiar danych potrzebnych do przechowywania niezbędnych informacji do 5 bajtów. Jeśli nie do 4. Wada jest taka sama jak w poprzednim przypadku - jeśli przechowujesz spakowaną datę, potrzebne są metody konwersji. Ale chcę to zrobić w ten sposób: przechowywać go w oddzielnych polach i serializować w formie spakowanej. Tutaj sensowne jest użycie Externalizable
:
// data is packed into 5 bytes:
// 3 2 1
// 10987654321098765432109876543210
// hhhhhmmmmmmssssssdddddMMMMyyyyyy yyyyyyyy
public void writeExternal(ObjectOutput out){
int packed = 0;
packed += ((int)hours) << 27;
packed += ((int)minutes) << 21;
packed += ((int)seconds) << 15;
packed += ((int)day) << 10;
packed += ((int)month) << 6;
packed += (((int)year) >> 8) & 0x3F;
out.writeInt(packed);
out.writeByte((byte)year);
}
public void readExternal(ObjectInput in){
int packed = in.readInt();
year = in.readByte() & 0xFF;
year += (packed & 0x3F) << 8;
month = (packed >> 6) & 0x0F;
day = (packed >> 10) & 0x1F;
seconds = (packed >> 15) & 0x3F;
minutes = (packed >> 21) & 0x3F;
hours = (packed >> 27);
}
Właściwie to wszystko. Po serializacji otrzymujemy narzut na klasę, dwa pola (zamiast 6) i 5 bajtów danych. Co jest już znacznie lepsze. Dalsze pakowanie można pozostawić wyspecjalizowanym bibliotekom. Podany przykład jest bardzo prosty. Jego głównym celem jest pokazanie, jak można wykorzystać zaawansowaną serializację. Chociaż moim zdaniem możliwy wzrost ilości serializowanych danych nie jest główną zaletą. Główną zaletą oprócz elastyczności... (płynnie przejdź do następnej sekcji...) Link do źródła: Serializacja taka jaka jest
GO TO FULL VERSION