JavaRush /Java-Blog /Random-DE /Serialisierung wie sie ist. Teil 1
articles
Level 15

Serialisierung wie sie ist. Teil 1

Veröffentlicht in der Gruppe Random-DE
Auf den ersten Blick scheint die Serialisierung ein trivialer Prozess zu sein. Was könnte einfacher sein? Die Klasse zur Implementierung der Schnittstelle deklariert java.io.Serializable– und das war's. Sie können die Klasse problemlos serialisieren. Serialisierung wie sie ist.  Teil 1 - 1Theoretisch stimmt das. In der Praxis gibt es viele Feinheiten. Sie hängen mit der Leistung, der Deserialisierung und der Klassensicherheit zusammen. Und mit vielen weiteren Aspekten. Solche Feinheiten werden besprochen. Dieser Artikel kann in die folgenden Teile unterteilt werden:
  • Feinheiten der Mechanismen
  • Warum wird es benötigt?Externalizable
  • Leistung
  • andererseits
  • Datensicherheit
  • ObjektserialisierungSingleton
Kommen wir zum ersten Teil –

Feinheiten der Mechanismen

Zunächst einmal eine kurze Frage. Wie viele Möglichkeiten gibt es, ein Objekt serialisierbar zu machen? Die Praxis zeigt, dass mehr als 90 % der Entwickler diese Frage ungefähr gleich beantworten (bis auf den Wortlaut) – es gibt nur einen Weg. Mittlerweile gibt es zwei davon. Nicht jeder erinnert sich an den zweiten, geschweige denn, er sagt irgendetwas Verständliches über seine Eigenschaften. Was sind diese Methoden? Jeder erinnert sich an den ersten. Dies ist die bereits erwähnte Umsetzung java.io.Serializableund erfordert keinen Aufwand. Die zweite Methode ist ebenfalls die Implementierung einer Schnittstelle, aber eine andere: java.io.Externalizable. Im Gegensatz zu java.io.Serializable, enthält es zwei Methoden, die implementiert werden müssen – writeExternal(ObjectOutput)und readExternal(ObjectInput). Diese Methoden enthalten die Serialisierungs-/Deserialisierungslogik. Kommentar.SerializableIm Folgenden beziehe ich mich manchmal auf Serialisierung mit Implementierung als Standard und Implementierung Externalizableals erweitert. Ein andererKommentar. Ich gehe jetzt bewusst nicht auf Standardoptionen zur Serialisierungssteuerung wie das Definieren von readObjectund ein writeObject, weil Ich denke, diese Methoden sind etwas falsch. Diese Methoden sind nicht in der Schnittstelle definiert Serializableund dienen vielmehr dazu, die Einschränkungen zu umgehen und die Standardserialisierung flexibel zu gestalten. ExternalizableVon Anfang an sind Methoden eingebaut, die Flexibilität ermöglichen . Stellen wir noch eine Frage. Wie funktioniert die Standard-Serialisierung eigentlich mit java.io.Serializable? Und es funktioniert über die Reflection API. Diese. Die Klasse wird als eine Reihe von Feldern geparst, von denen jedes in den Ausgabestream geschrieben wird. Ich denke, es ist klar, dass dieser Vorgang hinsichtlich der Leistung nicht optimal ist. Wie viel genau, erfahren wir später. Es gibt noch einen weiteren wesentlichen Unterschied zwischen den beiden genannten Serialisierungsmethoden. Nämlich im Deserialisierungsmechanismus. Bei Verwendung Serializableerfolgt die Deserialisierung wie folgt: Für ein Objekt wird Speicher zugewiesen, woraufhin seine Felder mit Werten aus dem Stream gefüllt werden. Der Konstruktor des Objekts wird nicht aufgerufen. Hier müssen wir diese Situation separat betrachten. Okay, unsere Klasse ist serialisierbar. Und seine Eltern? Völlig optional! Wenn Sie außerdem eine Klasse erben Object, ist die übergeordnete Klasse definitiv NICHT serialisierbar. Und obwohl Objectwir nichts über Felder wissen, können sie durchaus in unseren eigenen Elternklassen existieren. Was wird mit ihnen passieren? Sie gelangen nicht in den Serialisierungsstream. Welche Werte werden sie bei der Deserialisierung annehmen? Schauen wir uns dieses Beispiel an:
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;
        }
    }
}
Es ist transparent – ​​wir haben eine nicht serialisierbare übergeordnete Klasse und eine serialisierbare untergeordnete Klasse. Und das passiert:
Creating...
Parent::Constructor
Child::Constructor
Serializing...
Deserializing...
Parent::Constructor
c1.i=1
c1.field=5
Das heißt, während der Deserialisierung wird der Konstruktor ohne Parameter der übergeordneten, NICHT serialisierbaren Klasse aufgerufen . Und wenn kein solcher Konstruktor vorhanden ist, tritt bei der Deserialisierung ein Fehler auf. Der Konstruktor des untergeordneten Objekts, das wir deserialisieren, wird nicht aufgerufen, wie oben erwähnt. So verhalten sich Standardmechanismen bei der Verwendung Serializable. Bei der Verwendung Externalizableist die Situation anders. Zuerst wird der Konstruktor ohne Parameter aufgerufen, und dann wird die Methode readExternal für das erstellte Objekt aufgerufen, die tatsächlich alle seine Daten liest. Daher muss jede Klasse, die die Externalizable-Schnittstelle implementiert, einen öffentlichen Konstruktor ohne Parameter haben! Da außerdem alle Nachkommen einer solchen Klasse auch zur Implementierung der Schnittstelle in Betracht gezogen werden Externalizable, müssen sie auch über einen parameterlosen Konstruktor verfügen! Gehen wir weiter. Es gibt einen Feldmodifikator wie transient. Das bedeutet, dass dieses Feld nicht serialisiert werden sollte. Wie Sie jedoch selbst verstehen, betrifft diese Anweisung nur den Standard-Serialisierungsmechanismus. Bei Verwendung Externalizablemacht sich niemand die Mühe, dieses Feld zu serialisieren und zu subtrahieren. Wenn ein Feld als transient deklariert ist, nimmt es beim Deserialisieren des Objekts den Standardwert an. Ein weiterer eher subtiler Punkt. Bei der Standardserialisierung staticwerden Felder mit dem Modifikator nicht serialisiert. Dementsprechend ändert dieses Feld nach der Deserialisierung seinen Wert nicht. Natürlich macht sich während der Implementierung Externalizableniemand die Mühe, dieses Feld zu serialisieren und zu deserialisieren, aber ich empfehle dringend, dies nicht zu tun, weil Dies kann zu subtilen Fehlern führen. Felder mit einem Modifikator finalwerden wie normale Felder serialisiert. Mit einer Ausnahme können sie bei Verwendung von Externalizable nicht deserialisiert werden. Weil final-поляsie im Konstruktor initialisiert werden müssen und es danach nicht mehr möglich ist, den Wert dieses Felds in readExternal zu ändern. Wenn Sie ein Objekt mit einem -Feld serialisieren müssen final, müssen Sie daher nur die Standardserialisierung verwenden. Ein weiterer Punkt, den viele Menschen nicht kennen. Die Standardserialisierung berücksichtigt die Reihenfolge, in der Felder in einer Klasse deklariert werden. Auf jeden Fall war dies in früheren Versionen der Fall; in der JVM-Version 1.6 der Oracle-Implementierung ist die Reihenfolge nicht mehr wichtig, sondern der Typ und der Name des Felds. Es ist sehr wahrscheinlich, dass die Zusammensetzung der Methoden Auswirkungen auf den Standardmechanismus hat, obwohl die Felder im Allgemeinen gleich bleiben können. Um dies zu vermeiden, gibt es den folgenden Mechanismus. Zu jeder Klasse, die die Schnittstelle implementiert Serializable, wird in der Kompilierungsphase ein weiteres Feld hinzugefügt:private static final long serialVersionUID. Dieses Feld enthält die eindeutige Versionskennung der serialisierten Klasse. Sie wird basierend auf dem Inhalt der Klasse berechnet – Felder, ihre Deklarationsreihenfolge, Methoden, ihre Deklarationsreihenfolge. Dementsprechend ändert sich bei jeder Änderung in der Klasse der Wert dieses Felds. Dieses Feld wird bei der Serialisierung der Klasse in den Stream geschrieben. Dies ist übrigens vielleicht der einzige mir bekannte Fall, in dem staticein -Feld serialisiert wird. Bei der Deserialisierung wird der Wert dieses Feldes mit dem der Klasse in der virtuellen Maschine verglichen. Wenn die Werte nicht übereinstimmen, wird eine Ausnahme wie diese ausgelöst:
java.io.InvalidClassException: test.ser2.ChildExt;
    local class incompatible: stream classdesc serialVersionUID = 8218484765288926197,
                                   local class serialVersionUID = 1465687698753363969
Es gibt jedoch eine Möglichkeit, diese Prüfung zu umgehen, wenn nicht sogar zu umgehen. Dies kann nützlich sein, wenn der Satz von Klassenfeldern und ihre Reihenfolge bereits definiert sind, die Klassenmethoden sich jedoch ändern können. In diesem Fall ist die Serialisierung nicht gefährdet, der Standardmechanismus erlaubt jedoch keine Deserialisierung von Daten mithilfe des Bytecodes der geänderten Klasse. Aber wie gesagt, er kann getäuscht werden. Definieren Sie nämlich manuell das Feld in der Klasse private static final long serialVersionUID. Im Prinzip kann der Wert dieses Feldes absolut beliebig sein. Manche Leute ziehen es vor, es auf das Datum festzulegen, an dem der Code geändert wurde. Manche verwenden sogar 1L. Um den Standardwert (den intern berechneten Wert) zu erhalten, können Sie das im SDK enthaltene Dienstprogramm serialver verwenden. Sobald der Wert des Felds auf diese Weise definiert ist, ist er festgelegt, sodass eine Deserialisierung immer zulässig ist. Darüber hinaus erschien in Version 5.0 ungefähr Folgendes in der Dokumentation: Es wird dringend empfohlen, dass alle serialisierbaren Klassen dieses Feld explizit deklarieren, da die Standardberechnung sehr empfindlich auf Details der Klassenstruktur reagiert, die je nach Compiler-Implementierung variieren können. und somit unerwartete InvalidClassExceptionFolgen haben. Deserialisierung. Es ist besser, dieses Feld als zu deklarieren private, weil es bezieht sich ausschließlich auf die Klasse, in der es deklariert ist. Obwohl der Modifikator in der Spezifikation nicht angegeben ist. Betrachten wir nun diesen Aspekt. Nehmen wir an, wir haben diese Klassenstruktur:
public class A{
    public int iPublic;
    protected int iProtected;
    int iPackage;
    private int iPrivate;
}

public class B extends A implements Serializable{}
Mit anderen Worten, wir haben eine Klasse, die von einem nicht serialisierbaren Elternteil geerbt wurde. Ist es möglich, diese Klasse zu serialisieren und was wird dafür benötigt? Was passiert mit den Variablen der übergeordneten Klasse? Die Antwort ist diese. Ja, BSie können eine Instanz einer Klasse serialisieren. Was wird dafür benötigt? Die Klasse benötigt jedoch Aeinen Konstruktor ohne Parameter publicoder protected. AAnschließend werden bei der Deserialisierung alle Klassenvariablen mit diesem Konstruktor initialisiert. Die Klassenvariablen Bwerden mit den Werten aus dem serialisierten Datenstrom initialisiert. Theoretisch ist es möglich, in einer Klasse die Methoden zu definieren B, über die ich am Anfang gesprochen habe – readObjectund writeObject, –, zu Beginn die (De-)Serialisierung von Klassenvariablen Bdurch durchzuführen in.defaultReadObject/out.defaultWriteObjectund dann die (De-)Serialisierung verfügbarer Variablen durchzuführen aus der Klasse A(in unserem Fall sind dies iPublic, iProtectedund iPackage, wenn Bes sich im selben Paket wie befindet A). Meiner Meinung nach ist es jedoch besser, hierfür die erweiterte Serialisierung zu verwenden. Der nächste Punkt, den ich ansprechen möchte, ist die Serialisierung mehrerer Objekte. Nehmen wir an, wir haben die folgende Klassenstruktur:
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;}
}
Serialisierung wie sie ist.  Teil 1 - 2Was passiert, wenn Sie eine Instanz der Klasse serialisieren A? Es wird eine Instanz der Klasse mitgezogen B, die wiederum eine Instanz mitzieht C, die einen Verweis auf die Instanz hat A, dieselbe, mit der alles begann. Teufelskreis und unendliche Rekursion? Zum Glück nein. Schauen wir uns den folgenden Testcode an:
// 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));
Was machen wir? Wir erstellen eine Instanz der Klassen A, Bund C, verknüpfen sie miteinander und serialisieren dann jede von ihnen. Dann deserialisieren wir sie wieder und führen eine Reihe von Prüfungen durch. Was wird als Ergebnis passieren:
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
Was können Sie also aus diesem Test lernen? Erste. Objektreferenzen nach der Deserialisierung unterscheiden sich von Referenzen davor. Mit anderen Worten: Bei der Serialisierung/Deserialisierung wurde das Objekt kopiert. Diese Methode wird manchmal zum Klonen von Objekten verwendet. Die zweite Schlussfolgerung ist bedeutsamer. Beim Serialisieren/Deserialisieren mehrerer Objekte mit Querverweisen bleiben diese Verweise nach der Deserialisierung gültig. Mit anderen Worten: Wenn sie vor der Serialisierung auf ein Objekt zeigten, zeigen sie nach der Deserialisierung auch auf ein Objekt. Noch ein kleiner Test zur Bestätigung:
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));
Ein Klassenobjekt Bhat eine Referenz auf ein Klassenobjekt C. Bei der Serialisierung bwird es zusammen mit einer Instanz der Klasse serialisiert С, woraufhin dieselbe Instanz von c dreimal serialisiert wird. Was passiert nach der Deserialisierung?
b1.getC()==c1: true
c1==c2: true
c1==c3: true
Wie Sie sehen, stellen alle vier deserialisierten Objekte tatsächlich ein Objekt dar – die Verweise darauf sind gleich. Genau so, wie es vor der Serialisierung war. Ein weiterer interessanter Punkt: Was passiert, wenn wir gleichzeitig Externalizableund implementieren Serializable? Wie in dieser Frage – Elefant gegen Wal – wer wird wen besiegen? Wird überwinden Externalizable. Der Serialisierungsmechanismus prüft zunächst das Vorhandensein und erst dann das Vorhandensein. SerializableWenn also Klasse B, die implementiert Serializable, von Klasse A erbt, die implementiert Externalizable, werden die Felder der Klasse B nicht serialisiert. Der letzte Punkt ist die Vererbung. Beim Erben von einer Klasse, die implementiert Serializable, müssen keine zusätzlichen Maßnahmen ergriffen werden. Die Serialisierung wird auch auf die untergeordnete Klasse ausgedehnt. Wenn Sie von einer Klasse erben, die implementiert Externalizable, müssen Sie die Methoden readExternal und writeExternal der übergeordneten Klasse überschreiben. Andernfalls werden die Felder der untergeordneten Klasse nicht serialisiert. In diesem Fall müssen Sie daran denken, die übergeordneten Methoden aufzurufen, da sonst die übergeordneten Felder nicht serialisiert werden. * * * Wir sind wahrscheinlich mit den Details fertig. Es gibt jedoch ein Problem globaler Natur, das wir nicht angesprochen haben. Nämlich -

Warum brauchen Sie Externalizable?

Warum brauchen wir überhaupt eine erweiterte Serialisierung? Die Antwort ist einfach. Erstens bietet es viel mehr Flexibilität. Zweitens kann es häufig zu erheblichen Zuwächsen hinsichtlich des Volumens serialisierter Daten führen. Drittens gibt es einen Aspekt wie die Leistung, über den wir weiter unten sprechen werden . Mit Flexibilität scheint alles klar zu sein. Tatsächlich können wir die Serialisierungs- und Deserialisierungsprozesse nach Belieben steuern, was uns unabhängig von Änderungen in der Klasse macht (wie ich gerade oben sagte, können Änderungen in der Klasse die Deserialisierung stark beeinflussen). Daher möchte ich noch ein paar Worte zum Lautstärkegewinn sagen. Nehmen wir an, wir haben die folgende Klasse:
public class DateAndTime{

  private short year;
  private byte month;
  private byte day;
  private byte hours;
  private byte minutes;
  private byte seconds;

}
Der Rest ist unwichtig. Die Felder könnten vom Typ int sein, dies würde jedoch nur die Wirkung des Beispiels verstärken. In Wirklichkeit können die Felder jedoch intaus Leistungsgründen eingegeben werden. Auf jeden Fall ist der Punkt klar. Die Klasse stellt ein Datum und eine Uhrzeit dar. Es ist für uns vor allem unter dem Gesichtspunkt der Serialisierung interessant. Am einfachsten wäre es vielleicht, einen einfachen Zeitstempel zu speichern. Es ist vom Typ long, d.h. Bei der Serialisierung würde es 8 Bytes dauern. Darüber hinaus erfordert dieser Ansatz Methoden zur Umwandlung von Komponenten in einen Wert und zurück, d. h. – Produktivitätsverlust. Der Vorteil dieses Ansatzes ist ein völlig verrücktes Datum, das in 64 Bit passt. Dies ist ein enormer Sicherheitsspielraum, der in der Realität meist nicht benötigt wird. Die oben angegebene Klasse benötigt 2 + 5*1 = 7 Bytes. Plus Overhead für die Klasse und 6 Felder. Gibt es eine Möglichkeit, diese Daten zu komprimieren? Sicher. Sekunden und Minuten liegen im Bereich 0-59, d.h. Um sie darzustellen, genügen 6 statt 8 Bits. Stunden – 0-23 (5 Bits), Tage – 0-30 (5 Bits), Monate – 0-11 (4 Bits). Insgesamt, alles ohne Berücksichtigung des Jahres - 26 Bit. Es sind noch 6 Bits für die Größe von int übrig. Theoretisch kann dies in manchen Fällen für ein Jahr reichen. Wenn nicht, erhöht das Hinzufügen eines weiteren Bytes die Größe des Datenfelds auf 14 Bit, was einen Bereich von 0-16383 ergibt. Dies ist in realen Anwendungen mehr als ausreichend. Insgesamt haben wir die Größe der zur Speicherung der notwendigen Informationen erforderlichen Daten auf 5 Byte reduziert. Wenn nicht bis zu 4. Der Nachteil ist der gleiche wie im vorherigen Fall – wenn Sie das Datum gepackt speichern, sind Umrechnungsmethoden erforderlich. Aber ich möchte es so machen: in separaten Feldern speichern und in verpackter Form serialisieren. Hier ist es sinnvoll zu verwenden 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);
}
Eigentlich ist das alles. Nach der Serialisierung erhalten wir einen Overhead pro Klasse, zwei Felder (statt 6) und 5 Byte Daten. Was schon deutlich besser ist. Die weitere Verpackung kann Fachbibliotheken überlassen werden. Das gegebene Beispiel ist sehr einfach. Sein Hauptzweck besteht darin, zu zeigen, wie erweiterte Serialisierung verwendet werden kann. Obwohl der mögliche Gewinn an serialisierten Datenmengen meiner Meinung nach bei weitem nicht der Hauptvorteil ist. Der Hauptvorteil neben der Flexibilität... (fahren Sie reibungslos mit dem nächsten Abschnitt fort...) Link zur Quelle: Serialisierung wie sie ist
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION