JavaRush /Blog Java /Random-PL /Serializacja taka jaka jest. Część 1
articles
Poziom 15

Serializacja taka jaka jest. Część 1

Opublikowano w grupie Random-PL
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. Serializacja taka jaka jest.  Część 1 - 1Teoretycznie 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ówSingleton
Przejdźmy do pierwszej części –

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ż implementacja java.io.Serializablei nie wymaga żadnego wysiłku. Druga metoda to także implementacja interfejsu, ale inna: java.io.Externalizable. W przeciwieństwie do java.io.Serializabletego zawiera dwie metody, które należy wdrożyć - writeExternal(ObjectOutput)i readExternal(ObjectInput). Metody te zawierają logikę serializacji/deserializacji. Komentarz.SerializableW dalszej części będę czasami odnosił się do serializacji z implementacją w standardzie i implementacją Externalizablejako rozszerzoną. Innykomentarz. Celowo nie poruszam teraz takich standardowych opcji kontroli serializacji jak definiowanie readObjecti writeObject, ponieważ Myślę, że te metody są nieco błędne. Metody te nie są zdefiniowane w interfejsie Serializablei w rzeczywistości służą do obejścia ograniczeń i uelastycznienia standardowej serializacji. ExternalizableOd 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 Serializabledeserializacja 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ż Objectnie 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 Externalizablesytuacja 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, Externalizablenikt 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 staticnie są serializowane. W związku z tym po deserializacji pole to nie zmienia swojej wartości. Oczywiście podczas implementacji Externalizablenikt 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 finalsą 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 finalpole -, 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 Serializablena 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 staticpole - 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 InvalidClassExceptionkonsekwencje 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, Bmożesz serializować instancję klasy. Co jest do tego potrzebne? Ale klasa musi Amieć konstruktor bez parametrów, publiclub protected. Następnie podczas deserializacji wszystkie zmienne klasy Azostaną zainicjowane przy użyciu tego konstruktora. Zmienne klasy Bzostaną zainicjalizowane wartościami z serializowanego strumienia danych. Teoretycznie możliwe jest zdefiniowanie w klasie Bmetod, o których mówiłem na początku - readObjecti writeObject, - na początku których należy wykonać (des-)serializację zmiennych klasy Bpoprzez in.defaultReadObject/out.defaultWriteObject, a następnie (de-)serializację dostępnych zmiennych z klasy A(w naszym przypadku są to iPublic, iProtectedi iPackage, jeśli Bznajduje 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;}
}
Serializacja taka jaka jest.  Część 1 - 2Co się stanie, jeśli serializujesz instancję klasy A? Przeciągnie instancję klasy B, która z kolei przeciągnie instancję Cmają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 Ai łączymy je ze sobą, a następnie serializujemy każdą z nich B. CNastę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 Bzawiera odwołanie do obiektu klasy C. Kiedy jest serializowany, bjest 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 Externalizablei 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ść, Serializablewię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 intze 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
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION