Galileo Computing <openbook>
Galileo Computing - Programming the Net
Galileo Computing - Programming the Net


Java 2 von Friedrich Esser
Designmuster und Zertifizierungswissen
Zum Katalog
gp Kapitel 12 Serialisierung
  gp 12.1 Serialisierung: Kommunikation auf Basis von Objekten
  gp 12.2 Grundlagen der Serialisierung
  gp 12.3 Übertragungs-Protokolle
  gp 12.4 Einfache Anpassungen von Serializable
  gp 12.5 Klassen-Evolution
  gp 12.6 Anpassungen der Object-Streams
  gp 12.7 Zusammenfassung
  gp 12.8 Testfragen20

Kapitel 12 Serialisierung

Serialisieren bzw. Deserialisieren von Objekten ist ein Teilbereich von Java-I/O, geht aber weit über den Rahmen der traditionellen Kommunikation hinaus.
Hinter dem Begriff »Serialisierung« verbergen sich diverse Ebenen der Kommunikation auf Basis von Objekten.
Im Idealfall kann man durch Markieren der Klasse einfach alles der JVM überlassen. Im anderen Fall übernimmt man den gesamten Prozess des Marshalings bzw. Unmarshalings komplett.
In jedem Fall muss man sich über die Konsequenzen seiner Entscheidung in Bezug auf Vererbung, referenzierte Klassen, Klassen-Versionen und Sicherheiten im Klaren sein.


Galileo Computing

12.1 Serialisierung: Kommunikation auf Basis von Objekten  downtop

Einführung

Serialisierung ist der synonym verwendete Begriff für Objekt-Streams, d.h. das Lesen und Schreiben von Objekten. Serialisierung ist nicht auf singuläre Objekte beschränkt, da diese ja wieder andere Objekte referenzieren können.

Übertragen von Objekt-Graphen

Von primitiven Typen einmal abgesehen, müssen beliebige Objekt-Gebilde, präziser Objekt-Graphen, in ihrem aktuellen Zustand serialisiert und in einem Kanal übertragen werden.

gp  Das Hauptziel der Serialisierung ist ein einfacher standardisierter Default-Mechanismus zur Übertragung von Objekten, der stufenweise erweitert oder auch ersetzt werden kann.

ObjectOutput(Stream)
ObjectInput(Stream)

Die Objekt-Kommunikation führt die JVM im Normalfall mit Hilfe von Instanzen der Klassen ObjectOutputStream bzw. ObjectInputStream aus. Die beiden zentralen Methoden in diesen beiden Klassen sind in den Interfaces ObjectOutput bzw. ObjectInput deklariert:

write/readObject:
Object-Byte-Stream

gp  writeObject(), das ein Objekt in einen Byte-Stream umwandelt.
gp  readObject(), das einen Byte-Stream in ein Objekt umwandelt.

Hierzu benötigen beide Methoden den vollen Zugriff auf das zu übertragende Objekt.

Da beide Klassen indirekt die Interfaces DataOutput bzw. DataInput implementieren, können sie auch alle primitiven Daten serialisieren.


Galileo Computing

12.2 Grundlagen der Serialisierung  downtop

Allen Protokollen und Mechanismen der Serialisierung liegt eine Prämisse zugrunde:

Objekt-Rekonstruktion aus einer Byte-Sequenz

gp  Der Empfänger muss anhand des erhaltenen Byte-Streams in der Lage sein, die ursprünglichen Objekte zu rekonstruieren.
gp  Dies sollte auch dann möglich sein, wenn bei der Deserialisierung nur eine kompatible Klassen-Version vorliegt.

Gerade der letzte Punkt ist für Klassenerweiterungen erforderlich, allerdings auch nicht einfach zu realisieren.

Serialisierungs-Framework

Das Framework besteht aus zwei Teilen (Abb. 12.1).

Framework zur Serialisierung


Abbildung
Abbildung 12.1   Interfaces und Klassen zur (De-)Serialisierung

Serialisierung beruht auf
Interfaces

Im Unterschied zum allgemeinen I/O- bzw. Streaming-Konzept (vgl. dazu Kapitel 11, Package java.io) beruht die Objekt-Kommunikation auf Interfaces. Somit ist auch eine komplett eigencodierte Serialisierung möglich.

Allgemein gilt:

gp  Klassen, deren Objekte serialisiert werden sollen, implementieren Serializable bzw. Externalizable.
gp  Klassen, die die eigentliche Kommunikation vornehmen, implementieren die Interfaces ObjectOutput bzw. ObjectInput.

Galileo Computing

12.2.1 Standard-Serialisierung  downtop

Zuerst werden die in der Plattform realisierten Standard-Serialisierungs-mechanismen besprochen, da sie zur Realisierung von Erweiterungen oder Ersatz unbedingt notwendig sind.

Das Marshaling bzw. Unmarshaling der Daten wird von Instanzen der Klasse ObjectOutputStream bzw. ObjectInputStream übernommen:

writeObject()
serialisiert

gp  Um ein Objekt zu serialisieren, wird es der Instanz-Methode writeObject() von ObjectOutputStream übergeben.

readObject() deserialisiert

gp  Um ein neues Objekt – eine Art von Klon – auf der anderen Seite des Kanals zu deserialisieren, wird es der Instanz-Methode readObject() von ObjectInputStream übergeben (siehe Abb. 12.2).

Abbildung
Abbildung 12.2   (De-)Serialisierung von Objekten

Prinzipielles Code-Muster

Zum Serialisieren verwendet man Referenzen der Interfaces ObjectOutput bzw. ObjectInput:

Standard-Code-Muster für Objekt-Übertragung

  // Senden eines Objekts serObjectIn der Klasse 
SerClass
  ObjectOutput oos= new ObjectOutputStream(byteOutStream);
  oos.writeObject(serObjectIn);
  // Empfang eines Objekts serObjectOut der Klasse 
SerClass
  ObjectInput ois= new ObjectInputStream(byteInStream);
  serObjectOut= (SerClass) ois.readObject(); 
                 

Der Cast in der letzten Zeile ist notwendig, da readObject() ein Object zurückliefert.

Grundsätzliche Serialisierungs-Regeln

Icon
Grundlegende Serialisierungs-Regeln

1. Es werden nur Objekte serialisiert, keine statischen (Klassen-)Felder.
2. Die Klassen dieser Objekte müssen eines der Interfaces Serializable oder Externalizable implementieren.
3. Findet die Kommunikation zwischen zwei unterschiedlichen Prozessen statt, müssen beide JVMs Zugriff auf die Klasse des Objekts haben, da kein Byte-Code der Klasse übertragen wird.

Zu 1: Ein Objekt kann nur deserialisiert werden, wenn seine Klasse geladen ist. Dann sind aber bereits die statischen Felder der Klasse initialisiert.

Zu 2: Bei Serializable handelt es sich um ein Marker-Interface.

Zu 3: Bei unterschiedlichen Prozessen, z.B. Netzwerkübertragung von Objekten, muss sichergestellt sein, dass die andere JVM ebenfalls Zugriff auf (kompatible) Klassen der übertragenen Objekte hat.

Primitive Typen

Serialisieren von Daten primitiver Typen

Neben Objekten können mit Hilfe der Methoden write<X>() und read<X>() der Interfaces DataOutput und DataInput auch primitive Typen übertragen werden (siehe Abb. 12.1 und 12.2).


Galileo Computing

12.3 Übertragungs-Protokolle  downtop

Selbst wenn nur Daten primitiver Typen übertragen werden, beruht die Serialisierung auf einem vereinbarten Protokoll.

Serialisierungs-Protokoll mit Header

gp  Das Serialisierungs-Protokoll legt anhand einer Grammatik den Aufbau eines Byte-Streams fest.

Den Anfang eines Byte-Streams bildet ein Header, der aus der short-Konstanten STREAM_MAGIC (0xaced) und der Version STREAM_VERSION (zurzeit aktuell 0x0005) besteht (siehe Beispiel in 12.2.2).


Galileo Computing

12.3.1 Protokoll zu primitiven Typen  downtop

Block-Data-Record für write<X>-Methoden

Die mittels write<X>() übertragenen Daten werden in einem Block, dem so genannten Block-Data-Record, mit Kennung und Länge übertragen.

Die verwendeten Symbole sind im Interface ObjectStreamConstants (Abb. 12.1) definiert.

Wie bereits bei der einfachen I/O kann es leider auch hier noch zu Übertragungsfehlern, d.h. Fehlinterpretationen der Daten kommen:

Fehlinterpretationen nicht ausgeschlossen

gp  Das Protokoll für die Serialisierung von Daten primitiver Typen mittels write<X>() bzw. read<X>() lässt keine zweifelsfreie Rekonstruktion zu.

Beispiele

Um Interna der Serialisierung besser zu verstehen, benötigt man eine Hex/ASCII-Darstellung der Byte-Streams. Hierzu verwenden wir im Folgenden die statische Methode toHexAsciiString() der Utility-Klasse Sniffer:

Sniffer:
Byte-Arrays in Hex/ASCII-
Darstellung

class Sniffer {
  private static char hex[]= {'0','1','2','3','4','5','6','7',
                              '8','9','a','b','c','d','e','f'};
  public static String toHexAsciiString 
(byte[] b,int lfAtPos){
    if (b==null || b.length==0) return "";
    StringBuffer sb= new StringBuffer(4*b.length);
    int i,r;
    for (i= 0; i<b.length;i++) {
      sb.append(hex[(b[i]>>>4)&0xF]).append(hex[b[i]&0xF])
                                    .append(' ');
      if((i+1)%lfAtPos==0) {
        for (int j=i-lfAtPos+1; j<=i; j++) {
          if (32<=b[j] && b[j]<=126) sb.append((char)b[j]);
          else sb.append('.');
        }
        sb.append('\n');
      }
    }
    if (0 < (r= b.length%lfAtPos)) {
      for (i= 0;i<(lfAtPos-r)*3;i++) sb.append(' ');
      for (i= b.length-r;i<b.length; i++) {
        if (32<=b[i] && b[i]<=126) sb.append((char)b[i]);
        else sb.append('.');
      }
    }
    else sb.deleteCharAt(sb.length()-1);
    return sb.toString();
  }
}

Die folgende Kommunikation mit ByteArrayOutputStream und ByteArrayInputStream ist rein speicherbasierend, sehr schnell und aus zwei Gründen interessant:

gp  Sie lässt die Analyse der Struktur des Byte-Streams durch die Sniffer-Klasse sehr einfach zu.
gp  Sie erlaubt ein einfaches Cloning im Sinne von Deep-Copy (siehe auch 6.10.2).

Byte-Stream eines Block-Data-Records

public class Test {
  public static void main(String[] args) {
    byte[] barr= null;
    ByteArrayOutputStream baos= new ByteArrayOutputStream();
    try {
      // schreibt bereits Header, siehe Erklärung unten
ObjectOutput oout= new ObjectOutputStream(baos); ¨
      oout.writeInt(7);
      oout.writeBoolean(false);
      oout.write((byte)49);
      oout.flush();             // schreibt nach baos

Sniffer im Einsatz

      // wandelt Byte-Stream in Byte-Array um
      barr= baos.toByteArray();
      // Hex/ASCII-Darstellung mit 16 Zeichen pro Zeile
      System.out.println(Sniffer.toHexAsciiString(barr,16));
      ObjectInput oin= new ObjectInputStream(
                         new ByteArrayInputStream(barr));
      System.out.println(oin.readInt());
      System.out.println(oin.readByte());                      ¦
      System.out.println(oin.readBoolean());                   Æ
      // System.out.println(oin.readChar()); 
                   Ø
    } catch (IOException e) { System.out.println(e); 
}
  }
}

Hex/ASCII:
Header, Block-Kennung und -Länge, Daten primitiver Typen

ac ed 00 05 77 06 00 00 00 07 00 31             
....w......1
7
0
true

Zu ¨: Wie am Anfang dieses Abschnitts beschrieben, bilden die ersten vier Bytes den Header. Dieser wird bereits bei der Anlage des ObjectOutputStream mit writeStreamHeader() herausgeschrieben. Deshalb muss die Anweisung im try-catch stehen.

Zum Block-Data-Record: Der Block-Anfang ist mit 77 (TC_BLOCKDATA) markiert, 06 ist dann die Block-Länge und anschließend folgen die Daten.

Zu write<X>, read<X>: Typ-Informationen sind im Stream nicht enthalten. Deshalb können die Zeilen ¦ und Æ z.B. auch durch die Zeile Ø ersetzt werden, ohne dass dieser Fehler erkannt werden kann. Dies würde dann das Zeichen 1 liefern.

Codierung von Zeichen

Zeichen-Codierung in UTF8

gp  Grundsätzlich werden alle Zeichen vom Typ char oder String als UTF8 im Stream codiert.

Galileo Computing

12.3.2 Protokolle zur Objekt-Serialisierung  downtop

Für Objekte ist das Protokoll sowie das Stream-Format verständlicherweise wesentlich komplexer.

Icon

Mit den Interfaces Serializable und Externalizable sind zwei unterschiedliche Protokolle verbunden, die festlegen wie Objekte übertragen werden. Zusätzlich zu den bereits o.a. grundsätzlichen Regeln gilt:

Grundlegende Protokoll-Regeln

1. Implementiert eine Klasse keine der beiden Interfaces, so führt der Versuch, ein Objekt dieser Klasse zu serialisieren, zu der Ausnahme NotSerializableException.
2. Für die Klassen, die Externalizable implementieren, überträgt das Protokoll nur den voll qualifizierten Klassennamen. Alle weiteren Informationen zu den Feldern müssen von der Klasse selbst im Byte-Stream mittels der beiden Methoden
    gp  public void writeExternal(ObjectOutput out)
    gp  public void readExternal(ObjectInput in)
im Interface Externalizable codiert werden.

Default-Serialisierung: Klassen
implementieren nichts außer Serializable

3. Für Klassen, die nur Serializable implementieren, legt das Protokoll automatisch die Reihenfolge und Identifizierung seiner Felder und seiner Superklassen fest, wobei
    gp  static deklarierte Klassen-Variablen nicht serialisiert werden
    gp  die Klasse die zu übertragenden Instanz-Felder selbst bestimmen kann

Da Externalizable das Interface Serializable erweitert (Abb. 12.1), ist dass »nur« in der letzten Regel wesentlich. Denn alle Klassen, die Externalizable implementieren, sind auch als Serializable markiert.

Externalizable vs. Serializable

Die zweite und dritte Regel hat eine wichtige Implikation:

Externalizable Klassen:
Einbahnstraße für Subklassen

gp  Für die Subklassen einer Klasse, die Externalizable implementiert, gibt es keine automatische Serialisierung mehr.

Der folgende Code ist syntaktisch korrekt, aber semantisch sinnlos:

  class E implements Externalizable         { /*...*/ 
}
  class D extends E implements Serializable 
{ /*...*/ }

Die Klasse D muss trotzdem die Serialisierung selbst übernehmen.


Galileo Computing

12.3.3 Protokoll zu Externalizable  downtop

Das Interface Externalizable deklariert zwei Methoden, die jeweils als Argument einen Object-Stream übergeben bekommen.

Externalizable:
kein Marker- Interface

public interface Externalizable extends Serializable 
{
  void writeExternal(ObjectOutput out) throws IOException;
  void readExternal (ObjectInput in)   throws IOException,
                                       ClassNotFoundException;
}

Aufgrund der Object-Streams out bzw. in stehen bei der Implementierung der Methoden somit alle Methoden zum Lesen und Schreiben von primitiven Typen und Objekten zur Verfügung.

Anforderungen an externalizable Klassen

Anforderungen an externalizable Klassen

Klassen, die bei der Objekt-Kommunikation eine vollständige Kontrolle über die Feld-Inhalte benötigen, müssen

gp  Externalizable mit den beiden Methoden writeExternal() bzw. readExternal() implementieren.
gp  einen public deklarierten No-Arg-Konstruktor enthalten.

Im Gegensatz zu rein Serializable-Klassen kann eine Externalizable-Klasse von außen beliebig instanziiert werden, was für das Design recht unangenehme Konsequenzen haben kann (siehe auch 12.4.2).

Protokoll-Ablauf zu Externalizable

Protokoll-Ablauf bei Externalizable

Da die Hauptarbeit der Objekt-Kommunikation bis auf den Klassennamen nicht automatisch erfolgt, ist das Protokoll eigentlich recht einfach.

Bei der Serialisierung wird

1. der voll qualifizierte Klassenname des Objekts ASCII-codiert (nicht in Unicode!) in den Stream geschrieben.3
2. die Methode writeExternal() der Klasse mit dem entsprechenenden ObjectOutput-Stream aufgerufen.

Bei der Deserialisierung wird umgekehrt

1. die Klasse anhand des im Stream enthaltenen Namens identifiziert.
2. eine Instanz der Klasse mit Hilfe des public No-Arg-Konstruktors erschaffen.
3. die Methode readExternal() der Klasse mit dem entsprechenenden ObjectInput-Stream aufgerufen.

Beliebige Manipulation der Feld-Informationen

Mit writeExternal() werden die gewünschten Felder des Objekts in den Stream geschrieben, wobei beliebige Manipulationen, z.B. Verschlüsselung der Daten, möglich sind.

Sollen interne Objekte übertragen werden, muss dies selbst kodiert werden.

Reihenfolge der Felder ist wichtig

Bei readExternal() ist dann darauf zu achten, dass die Reihenfolge der Felder exakt eingehalten wird und eventuelle Entschlüsselungen vorgenommen werden.

Externalizable und Felder von Superklassen

Externalizable:
Felder von Superklassen nicht implizit enthalten

gp  Die Felder aller Superklassen – selbst wenn diese als Serializable markiert sein sollten – müssen ebenfalls explizit übertragen werden.

Gemäß dem o.a. zweiten Punkt der Deserialisierung, werden zwar automatisch die Felder der Superklassen eines Objekts angelegt, dies sorgt aber nur für die Default-Initialisierung.

Enthält das zu übertragende Objekt davon abweichende Werte, sind diese ohne explizite Übertragung im deserialisierten Objekt nicht vorhanden.

Beispiel

Nachfolgend wird eine sehr einfache Klasse E angelegt:

package kap12;
import java.io.*;

Beispiel:
externalizable Klasse

class E implements Externalizable 
{
  byte b= 1;
  public E() {};                    // public notwendig!
  E(byte b)  { this.b= b; }
  public void writeExternal 
(ObjectOutput out) {
    try { out.writeByte(b); 
    } catch (Exception e) {System.out.println(e);} 
  }
  public void readExternal 
(ObjectInput in) {
    try { b= (byte) in.readByte(); 
    } catch (Exception e) {System.out.println(e);}
  }
}

Ein Objekt der Klasse E wird nun wieder (de-)serialisiert und der Byte-Stream mit Sniffer ausgegeben:

public class Test {
  public static void main(String[] args) {
    ObjectOutput oout= new ObjectOutputStream(baos);
    oout.writeObject(new E((byte)3)); oout.flush();
    barr= baos.toByteArray();
    System.out.println(Sniffer.toHexAsciiString(barr,16));
    ObjectInput oin= new ObjectInputStream(
                       new ByteArrayInputStream(barr));
    System.out.println(((E)oin.readObject()).b);
  }
}

Beispiel:
Protokoll- Bytes einer externalizable Klasse

ac ed 00 05 73 72 00 07 6b 61 70 31 32 2e 45 c1 
....sr..kap12.E.
b5 79 8a 99 51 65 29 0c 00 00 78 70 77 01 03 78 .y..Qe)...xpw..x
3

Erklärung: Nach dem obligatorischen 4-Byte-Header startet das Objekt im Stream mit TC_OBJECT (73).

Nach der Klassenkennung TC_CLASSDESC(72) folgt die Länge (0007) und der Name der Klasse (kap12.E) als ASCII-Zeichen.

Anschließend folgt ein 64-Bit-Hashcode (c1..29), der die Klassen eindeutig identifiziert.

Am Ende stehen dann die Feldwerte des Objekts in einem Block. In diesem Fall ist dies nur ein Byte (03), eingerahmt in TC_BLOCKDATA(77), Länge (01) und TC_ENDBLOCKDATA(78).

Fazit

Externalizable:
eigenes Protokoll zu Feldern

Auch bei Externalizable ist das Protokoll bis auf die eigentlichen Feld-Informationen vorgegeben. Die Daten der Felder sind aber frei manipulierbar.


Galileo Computing

12.3.4 Protokoll zu Serializable  downtop

Implementiert eine Klasse Serializable, so kann die Serialisierung völlig transparent von der JVM übernommen werden.

Icon

Allerdings stellt das Protokoll gewisse Anforderungen an die Objekte und ihre Klassen.

1. Anforderungen an eine Klasse

Anforderungen an serializable Klassen

Eine Klasse ist serializable , wenn

gp  keine ihrer Superklassen Externalizable implementiert und
gp  sie Zugriff auf den No-Arg-Konstruktor der ersten Superklasse hat, die nicht serialisierbar ist.

Anforderungen an serializable Objekte

2. Anforderungen an ein Objekt

Ein Objekt ist dann serializable, wenn

gp  seine Klasse serializable ist und
gp  seine Referenz-Felder entweder null sind oder Objekte von Klassen referenziert, die serialisierbar sind.

Default-Serialisierung

Default-
Serialisierung

Unter dem Begriff Default-Serialisierung versteht man das Standard-Protokoll einer serializable Klasse, die selbst keine Serialisierungs-Methoden implementiert.

It´s magic –
read/writeObject(): Zugriff auf private Felder

Das Standard-Protokoll verwendet hierzu writeObject() bzw. readObject() von ObjectOutputStream bzw. ObjectInputStream.

gp  Die Methoden writeObject() und readObject() haben dazu »magischen« Zugriff auch auf alle private deklarierten Felder der Objekte.

Prinzipieller Ablauf der Default-Serialisierung

Ablauf der Default-
Serialisierung

Der Serialisierungs-Prozess eines Objekts mit der Methode writeObject() läuft im Wesentlichen wie folgt ab :

1. Es werden der Klassen-Deskriptor, d.h. der Klassenname sowie die Namen aller nicht transient deklarierten Felder in den Stream geschrieben.
2. Die Werte der Felder werden mit der Methode defaultWriteObject() in den Stream geschrieben.

Transitive Hüllen-Operation

3. Dabei werden alle Objekte, die über Felder vom Objekt erreichbar sind, ebenfalls in den Stream geschrieben, wobei für Objekte, die bereits serialisiert sind, nur ein Handle abgelegt wird. Diese Operation wird kurz als transitive Hülle (transitive closure) bezeichnet.
4. Ist ein Objekt Instanz einer serializable Subklasse, wird für die serialisierbaren Superklassen ebenfalls writeObject() oder es werden ihre speziellen Serialisierungs-Methoden7 aufgerufen.

ObjectStreamClass zur Feldbeschreibung

ObjectStreamClass-Descriptor

Im Gegensatz zu Externalizable werden also zu jedem Objekt auch die Namen der übertragenen Felder im Stream geschrieben.

Zu Klassen- bzw. Feldbeschreibungen wird eine Instanz des Klassen-Deskriptors ObjectStreamClass serialisiert.

Standard-Objekte wie Object oder String werden allerdings nur als Ident in den Stream geschrieben (siehe Konstanten in ObjectStreamConstants).

Beispiel

Beispiel:
Klassen- Hierarchie mit Aggregation

Objekte der folgenden Klasse Square sind serializable, unabhängig davon, ob die Superklasse Figure serialisierbar ist oder nicht.

Point muss allerdings in jedem Fall serialisierbar sein (vgl. 12.3.4 »Anforderungen an ein Objekt«).


Abbildung
Abbildung 12.3   Klassen-Diagramm zum Beispiel
class Point implements 
Serializable {  int x,y; }
class Figure /* implements 
Serializable */ {             ¨
  protected Point base= new Point();
  public  Figure()             { this(0,0); }
  public  Figure(int x, int y) { base.x=x; base.y=y; }
}
class Square extends 
Figure implements Serializable {
  private Point p= new Point();
  transient public boolean clockwise= true;
  public Square (int x1, int y1, int x2, int y2) {
    super(x1,y1); p.x= x2; p.y= y2;
  }
  public String toString() {  
    return "["+ base.x+","+ base.y+";"+p.x+","+p.y+";"+
           clockwise+"]";
  }
}

Zu ¨: Klassen- bzw. Feld-Informationen und Werte zu Figure werden in den Stream nur aufgenommen, wenn die Klasse serialisierbar ist, z.B. Serializable implementiert.

Objekt-Default-Deserialisierung

Ablauf
der Default- Deserialisierung

Der Deserialisierungs-Prozess der Methode readObject() für ein Objekt läuft prinzipiell wie folgt ab:

1. Nach Deserialisierung der ObjectStreamClass-Instanz werden die Klassen- bzw. Feldbeschreibungen ausgewertet und die zugehörige Klasse wird im lokalen System geladen.
2. Es wird eine Instanz erzeugt.
    gp  Für externalizable Objekte wird der public No-Arg-Konstruktor aufgerufen und anschließend readExternal().
    gp  r serializable Objekte wird – falls notwendig – der No-Arg-Konstruktor der ersten nicht serialisierbaren Superklasse aufgerufen.
3. Die Felder werden ohne Aufruf eines Konstruktors oder Instanz-Initialisierers mit Hilfe der Methode defaultReadObject() mit Werten belegt.
    gp  Felder, für die es keinen Wert im Stream gibt, erhalten den Default-Wert.

Die neu erschaffene Instanz ist somit total unabhängig vom originalen Objekt. Abschließend noch eine Anmerkung:

EOFException bei Vertauschung

gp  Der Versuch, ein Objekt als primitiven Typ zu deserialisieren, führt zu der Ausnahme EOFException.

Beispiel

Die Ausgabe von Test hängt also davon ab, ob Figure (siehe oben, Zeile ¨) das Interface Serializable implementiert hat.

public class Test {
  public static void main(String[] args) {
    ByteArrayOutputStream baos= new ByteArrayOutputStream();
    ObjectOutput oout= new ObjectOutputStream(baos);
    oout.writeObject(new Square(1,2,3,4)); oout.flush();
    ObjectInput oin= new ObjectInputStream(
                       new ByteArrayInputStream(
                          baos.toByteArray()));
    System.out.println(((Square)oin.readObject())); 
    // Figure nicht serialisierbar :: [0,0;3,4;false]
// Figure serialisierbar :: [1,2;3,4;false]
} }

Galileo Computing

12.4 Einfache Anpassungen von Serializable  downtop

Anpassung der Serialisierungs-Mechanismen

Neben den Varianten Default-Serialisierung bzw. Serialisierung mittels Externalizable gibt es noch weitere – teilweise recht komplexe – Möglichkeiten der Protokoll-Anpassung.


Galileo Computing

12.4.1 serialPersistentFields: Ersatz für transient  downtop

serialPersistentFields überschreibt transient

Die Kennzeichnung der nicht serialisierbaren Instanz-Felder als transient ist zwar einfach, aber manchmal nicht flexibel genug.

Dieser Default-Mechanismus lässt sich mit Hilfe des speziellen private static final deklarierten Felds serialPerstistentFields innerhalb der serializable Klasse überschreiben.

serialPersistentFields: ein Code-Muster

gp  Das Feld serialPerstistentFields muss mit einem Array von ObjectStreamField-Instanzen initialisiert werden, die die Namen und Typen der serialisierbaren Instanz-Felder enthalten:
    class C implements Serializable {
      // ..Felder..
      private static final ObjectStreamField[] 
        serialPersistentFields = {
          new ObjectStreamField("fieldname", fieldType.class),
          //...
        };
      //...
    }

Beispiel

In der Klasse C werden trotz transient die Felder s und i serialisiert:

class C implements Serializable {
  transient String s= "abc";      // transient nutzlos
  transient int i= 2;             // dito
  private static final ObjectStreamField[] 
      serialPersistentFields = {
        new ObjectStreamField("s", String.class),
        new ObjectStreamField("i", int.class)
      };
}

Galileo Computing

12.4.2 Externalizable: Kapselung unmöglich   downtop

Keine Kapselung: gravierender Nachteil von Externalizable

Ein gravierender Nachteil der Implementierung von Externalizable liegt darin, dass die Methoden writeExternal() bzw. readExternal() public deklariert werden müssen.

gp  Die Externalizable-Methoden können nicht nur von der JVM zur Serialisierung, sondern für jeden anderen Zweck missbraucht werden.

Aus Sicht der Kapselung sind Anpassungen bzw. Erweiterungen des Default-Mechanismus sicherlich die bessere Wahl.


Galileo Computing

12.4.3 Klasseninterne Anpassung der Default-Serialisierung  downtop

Klassenintern read/write Object() überschreiben Default-Mechanismus

Mit Hilfe der klasseninternen private deklarierten Methoden writeObject() und readObject() kann eine serializable Klasse den Default-Mechanismus anpassen.

Dazu muss die Klasse diese Methoden mit folgender Signatur (mit oder ohne throws) implementieren:

class C implements Serializable {
defaultWriteObject(): Aufruf des Defaults

Code-Muster

  private void writeObject 
(ObjectOutputStream oout) 
               throws IOException {
    //...
    // oout.defaultWriteObject();
    //...
  }
  private void readObject 
(ObjectInputStream oin) 
               throws ClassNotFoundException, IOException {

defaultRead
Object(): Aufruf des Defaults

    //...
    // oin.defaultReadObject();
    //...
  }
}

Auch hier wieder:
It´s magic

Diese beiden Methoden sind wie ihre gleichnamigen Pendants in den beiden Object-Streams magisch.

gp  Da ObjectOutputStream bzw. ObjectInputStream als Argument übergeben wird, können mit Hilfe der Default-Methoden die Werte der »normalen« Felder serialisiert werden.

Beispiel (in drei Varianten)

Die Klasse C enthält jeweils drei Varianten zu writeObject() bzw. readObject(). Beide Methoden

read/writeObject():
drei interne Varianten

1. sind leer, d.h. ohne Anweisung.
2. rufen nur ihre Default-Methoden auf (zusätzlich Zeile ¨ bzw. Ø).
3. rufen ihre Default-Methoden auf und setzen Datum und Zeit des statischen Felds d (zusätzlich Zeile ¨ ¦ bzw. Æ Ø).
class C implements Serializable {
  static Date d;
  String s= "hi";
  int i= 7;
  private void writeObject (ObjectOutputStream oout) {
    try {
      // oout.defaultWriteObject();                           ¨
      // oout.writeObject(Calendar.getInstance().getTime());  ¦
    } catch (Exception e) {System.out.println(e);}
  }
  private void readObject (ObjectInputStream oin) {
    try {
      // oin.defaultReadObject();                             Æ
      // d= (Date) oin.readObject();                          Ø
    } catch (Exception e) {System.out.println(e);}
  }
  public String toString() { return d+","+s+","+i; }
}
public class Test {
  public static void main(String[] args) {
    byte[] barr= null;
    ByteArrayOutputStream baos= new ByteArrayOutputStream();
    ObjectOutput oout= new ObjectOutputStream(baos);
    oout.writeObject(new C()); oout.flush();
    barr= baos.toByteArray();
    System.out.println(Sniffer.toHexAsciiString(barr,16));
    ObjectInput oin= new ObjectInputStream(
                       new ByteArrayInputStream(
                                   baos.toByteArray()));
    System.out.println(oin.readObject());
  }
}

1. Variante

Es werden nur die Klassen- und Feld-Informationen übertragen, da eine Instanz der ObjectStreamClass serialisiert wird.

Es fehlen alle Werte. Die Ausgabe ist uninteressant und wird deshalb weggelassen.

2. Variante

Mit mehr Aufwand hat man praktisch die Default-Serialisierung nachgebildet. Auch der Stream-Inhalt ist nahezu identisch mit demjenigen der Default-Serialisierung.

ac ed 00 05 73 72 00 07 6b 61 70 31 32 2e 43 2c 
....sr..kap12.C,
92 61 0f 51 e2 bc ff 03 00 02 49 00 01 69 4c 00 .a.Q......I..iL.
01 73 74 00 12 4c 6a 61 76 61 2f 6c 61 6e 67 2f .st..Ljava/lang/
53 74 72 69 6e 67 3b 78 70 00 00 00 07 74 00 02 String;xp....t..
68 69 78                                        hix
null,hi,7

Es wurde nur das Stream-Flag SC_SERIALIZABLE (0x02) mit dem Stream-Flag SC_WRITE_METHOD (0x01) (per AND) zu 0x03 überlagert. Ein zusätzliches TC_ENDBLOCKDATA (0x78) terminiert die Werte.

3. Variante

Zusatz-
Funktionalität: Serialisieren von statischen Feldern

Eine zusätzliche Funktionalität ist eigentlich der Sinn der klasseninternen Implementation der beiden Methoden.

gp  In diesem Fall wird der Wert eines statischen Felds übertragen, was bei der Default-Serialisierung nicht möglich ist.10 
Thu Dec 07 20:31:34 GMT+01:00 2000,hi,7

Die Ausgabe zeigt die Übertragung und erfolgreiche Initialisierung des statischen Felds d, das in der zweiten Variante noch null war.


Galileo Computing

12.4.4 Broker-Pattern: Stream-Ersatzobjekte  downtop

Icon

Serialisierung ist ein zusätzlicher Dienst zu einer Klasse. Es ist nicht unbedingt vorteilhaft, diesen Dienst in der Klasse selbst zu implementieren.

Broker-Pattern: transparentes Service-Objekt

Der nachfolgende Mechanismus basiert auf dem Broker-Pattern.

gp  Ein Broker (Objekt-Makler) ist ein Service-Objekt, das für eine Klasse einen bestimmten Dienst transparent für die Clients abwickelt.

Im konkreten Fall wird der Serialisierungs-Dienst der Klasse nicht von ihr selbst abgewickelt, sondern einem Broker-Objekt überlassen.

gp  Es wird also kein Objekt der serializable Klasse selbst, sondern ein Broker-Objekt in den Stream geschrieben (Abb. 12.4).

Broker-Muster für die Serialisierung


Abbildung
Abbildung 12.4   Broker übernimmt Serialisierungs-Aufgabe

Vorteile eines Brokers

Broker-Vorteil

Die Klasse selbst wird vom Serialisierungs-Prozess entlastet. Die Aufgabe der Übernahme relevanter Objekt-Informationen sowie der Wiederherstellung des Objekts liegt ausschließlich beim Broker-Objekt.

Ohne die ursprüngliche Klasse ändern zu müssen, können in Broker-Objekten verschiedene Strategien der Übertragung angewendet werden.

Delegation an Broker

Broker-Delegation in der Server-Klasse:
writeReplace()

Damit der Broker-Mechanismus auch greift, muss die serialisierbare Klasse die folgende Methode (in der Regel transparent als private) implementieren:

   [public|protected|private] Object writeReplace() 
                              throws ObjectStreamException;

Wird nun ein Objekt dieser Klasse durch ObjectOutputStream serialisiert, ruft dieser die Methode writeReplace() auf und schreibt das Resultat als Broker-Objekt in den Byte-Stream.11 

gp  Da im Byte-Stream nur das Broker-Objekt enthalten ist, kann die Deserialisierung nur aus der Rekonstruktion des Broker-Objekts bestehen.

Die delegierende Klasse kann zwar auch Externalizable implementieren, dies ist aber nicht unbedingt sinnvoll, da sie dann zwei Methoden enthält, die nicht genutzt werden.

Broker-Implementation

Mechanismus in der Broker-Klasse:
readResolve()

Implementiert die serialisierbare Broker-Klasse die Methode

   [public|protected|private] Object readResolve() 
                              throws ObjectStreamException;

so ruft ObjectInputStream beim Deserialisieren die Methode readResolve() des Broker-Objekts auf.

Das Resultat dieser Methode ist dann in der Regel das rekonstruierte Objekt des Originals (der delegierenden Klasse).

Beziehung: Klasse vs. Broker

writeReplace() und readResolve() sind unabhängig

Die Methoden writeReplace() und readResolve() sind unabhängig voneinander, können somit auch einzeln eingesetzt werden.

Die Methode writeReplace() kann in einer serialisierbaren Klasse ohne Broker eingesetzt werden, um z.B. nur bestimmte Objekte der Klasse zu deserialisieren.

gp  Entgegen dem Eindruck der offiziellen Dokumentation kann das Broker-Objekt durchaus mit readResolve() ein beliebiges Objekt liefern, nicht unbedingt das der ursprünglichen Klasse.

Kritik

Broker-Lösung basiert nicht auf Interfaces

Der Broker-Mechanismus basiert nicht auf Interfaces, da die beiden Methoden dann nur public deklariert sein könnten.

Das aktuelle Interface-Konzept ist also – wieder einmal – nicht flexibel genug, d.h. wird hier durch Reflexion oder (magic) private-Zugriffe umgangen.

Beispiele

Die triviale Klasse C ist ihr eigener Broker, d.h., es wird nur wieder die Default-Serialisierung nachgebildet:

class C implements Serializable {
  private Object writeReplace() throws ObjectStreamException {
    return this;
  }
  private Object readResolve() throws ObjectStreamException {
    return this;
  }
}

Delegator-Klasse mit zwei Broker-Varianten

Zur Klasse Delegator werden mögliche Broker-Varianten vorgestellt:

class Delegator implements Serializable {
  private int i;
  public Delegator(int i) { this.i= i; }
  public int geti() { return i; }
  private Object writeReplace() throws ObjectStreamException {
    return new Broker(this);
  }
}

1. Broker-Variante

Der Broker deserialisiert einfach ein konstantes String-Objekt:

Deserialisierung eines konstanten String-Objekts

class Broker implements Serializable {
  public Broker(Delegator d) {}
  private Object readResolve() throws ObjectStreamException {
    return "Delegator?";
  }
}

2. Broker-Variante

Der Broker deserialisiert ein Delegator-Objekt im gleichen Zustand:

Deserialisierung eines Delegator-Objekts im gleichen Zustand

class Broker implements Serializable {
  private int i;
  public Broker(Delegator d) { i= d.geti()^0xaaaa; }
  private Object readResolve() throws ObjectStreamException {
    return new Delegator(i^0xaaaa);
  }
}

Galileo Computing

12.5 Klassen-Evolution  downtop

Klassen-Evolution: Versionswechsel nach
Serialisierung

Die Objekt-Kommunikation kann bedingt durch Zeitversatz (Serialisieren in Dateien) oder verschiedene JVMs auf unterschiedliche Klassen-Versionen treffen.

Natürlich trifft dieses Problem gleichermaßen auf serializable und externalizable Klassen zu. Die folgende Diskussion beschränkt sich aber ausschließlich auf Klassen, die Serializable implementieren.

SUID

(S)UID: Stream Unique Identifier

Serialisierte Objekte einer anderen Klassen-Version können nicht ohne spezielle Vorkehrungen deserialisiert werden.

Eindeutiger Hashcode der Klassen-Version

gp  Mit jedem Objekt wird nicht nur die zugehörige Klasse, sondern auch ein eindeutiges Ident – kurz UID – vom Typ long serialisiert, das nicht nur die Klasse, sondern auch jede Version der Klasse eindeutig identifiziert.

Icon

Das UID ist der Hashcode12  , berechnet aus allen relevanten Klassen-Informationen wie Name, Felder, Parameter, Modifier etc. und ändert sich somit mit jeder neuen Version.

Stream-
Kompatibilität

gp  Serializable Klassen mit gleichem Namen, die dieselbe UID haben, werden bei der (De-)Serialisierung als stream-kompatible angesehen.13 

Mit demselben UID wird ausgedrückt, dass die neue Klassen-Version den Client-Kontrakt der alten einhält (siehe Stream-Kompatibilität).

gp  Für stream-kompatible Versionen einer Klasse C kann man das UID manuell durch folgende Anweisung setzen:
   class C implements Serializable {
     // jede stream-kompatible Version hat 
dieselbe id

UID-Anweisung

     public static final long serialVersionUID= idL; 14 
     //...
   }

Der Wert von id ist nicht festgelegt, er muss nur identifizierend sein.

UID-Berechnung

Will man ein UID-konformes Ident vergeben, kann man diesen manuell durch das Utility-Programm serialver15  berechnen lassen oder im Programm mit Hilfe der Klasse ObjectStreamClass:

class C implements Serializable {

getSerialVersionUID()

  static public long uid() {
    return ObjectStreamClass.lookup(C.class)
                            .getSerialVersionUID();
} //... }

In der Praxis wird bereits die erste Version einer serializable Klasse, nachdem sie stabil ist, manuell mit einer UID versehen. Denn für jede Klasse, die kein serialVersionUID deklariert hat, wird sonst vor dem Streaming-Prozess der Hashcode berechnet.


Galileo Computing

12.5.1 Stream-Kompatibilität  downtop

Der Begriff stream-kompatibel soll im Weiteren kurz präzisiert werden. Es gibt hierzu zwei Aspekte, d.h. eine notwendige und eine hinreichende Bedingung.

Verhalten der Default-Serialisierung

Icon

Notwendig für eine stream-kompatible Klassen-Evolution ist die Frage, was die Default-Serialisierung beim Deserialisierungs-Prozess an Versionsänderungen toleriert:

Stream-kompatible Klassen

gp  Neue Felder: Sind Felder im Stream, d.h. in der alten Version, nicht enthalten, werden sie mit 0 oder null initialisiert (nicht mit ihren Initialisierungswerten!).
gp  Fehlende Felder: Felder im Stream, die es in der neuen Version nicht mehr gibt, werden einfach ignoriert.

Nicht toleriert, d.h. mit einer InvalidClassException bestraft, werden dagegen folgende Änderungen:

gp  Typ-Wechsel: Ein Feld mit gleichem Namen wechselt den Typ.
gp  Änderung der Serialisierungsart: Eine Klasse, die im Stream enthalten ist, ändert ihre Serialisierungsart.

Zusätzlich notwendiger Kontrakt

Eine erfolgreiche Deserialisierung ist zwar notwendig, aber in der Regel zur Einhaltung des Kontrakts bei der Klassen-Evolution nicht hinreichend.

Semantische Fehler durch Regel nicht ausgeschlossen

Vor allem die Default-Werte 0 bzw. null für fehlende Felder, der Wechsel eines Felds von static zu nicht static oder der Klasse innerhalb der Klassen-Hierarchie führen zu unangenehmen semantischen Fehlern.

Beispiel

In einer neuen Version der Klasse Evolve wird ein Feld hinzugefügt bzw. weggelassen:

Klasse: 1. Version

class Evolve implements Serializable {
  public static final long serialVersionUID= 1; 
  public static final String ss= "V1";
  String s= "hi";                         // fällt weg
  private int i= 1;
}

Eine Instanz von Evolve wird in die Datei Version.ser geschrieben:

ObjectOutput oout= new ObjectOutputStream(
              new FileOutputStream("C:/temp/Version.ser"));
oout.writeObject(new Evolve());
oout.close();

Neue Version der Klasse Evolve:

Klasse-Evolution:
ein Feld fehlt, ein Feld hinzugefügt

class Evolve implements Serializable 
  public static final long serialVersionUID= 1;
  public static final String ss= "V2";
  private int i= 1;
  private float f=1.0f;                   // neues Feld
  public String toString() { return ss+"|"+i+"|"+f; 
}
}

Die alte Instanz wird mit der neuen Version von Evolve deserialisiert:

ObjectInput oin= new ObjectInputStream(
                  new FileInputStream("C:/temp/Version.ser"));
System.out.println(((Evolve)oin.readObject())); // :: V2|1|0.0

Die Ausgabe bestätigt, dass überflüssige Felder im Stream ignoriert werden, und neue Felder wie f ohne Aufruf des Instanz-Initialisierers erzeugt und mit Null initialisiert werden.


Galileo Computing

12.6 Anpassungen der Object-Streams  downtop

Betrachtet man die Signatur von writeObject() und readObject() der Object-Streams, so akzeptieren diese Argumente vom Typ Object.

Da alle einfachen Anpassungen der Serialisierung wie die bisher besprochenen auf dem Default-Mechanismus aufsetzen, wäre eigentlich ein Parameter vom Typ Serializable logischer.

Subklassen von ObjectInputStream bzw. ObjectOutputStream

Der Grund für diese Entscheidung ist die Flexibilität bei den Subklassen ObjectOutputStream bzw. ObjectInputStream:

gp  Subklassen der Object-Streams sind dann notwendig, wenn Objekte von Klassen übertragen werden müssen, die nicht serialisierbar sind.

Dies führt zu drei weiteren Serialisierungs-Mechanismen.


Galileo Computing

12.6.1 annotateClass() und resolveClass() für Klassendaten  downtop

Übertragen von einmaligen Klassen-Informationen

Statische Felder werden bereits beim Laden der Klasse angelegt und initialisiert, d.h. vor jeder Objekt-Erzeugung bzw. -Übertragung.

gp  Allgemein macht eine Übertragung von Klassen-Informationen in jedem Objekt wenig Sinn, wäre redundant und ineffizient.

Der Austausch von Klassen-Informationen sollte nur einmal erfolgen.

Overriding von annotatedClass()

Hierzu müssen die Methoden annotatedClass() aus ObjectOutputStream und resolveClass() aus ObjectInputStream in den entsprechenden Subklassen dieser Streams überschrieben werden.

Diese Methode annotatedClass() wird ausgeführt, nachdem die Klassen-Beschreibung in den Stream geschrieben wurde, also vor der Übertragung der Objekte.

Overriding von resolveClass()

Mit resolveClass() müssen dann umgekehrt die Informationen wieder gelesen werden.

Beispiel

Es soll der Wert des statischen Felds s der Klasse A übertragen werden.

class A implements Serializable {
  static String s;  
  char c;
  A(char c) { this.c= c; }
}

Hierzu werden Subklassen der Object-Streams angelegt und die o.a. Methoden überschrieben.

Subklasse von ObjectOutputStream mit
annotatedClass()

class MyObjectOutputStream extends ObjectOutputStream 
{
  public MyObjectOutputStream(OutputStream out) 
                                     throws IOException { 
    super(out); 
  }
  protected void annotateClass(Class c) throws IOException {
    // überschreibt die leere Methode in ObjectOutputStream
// schreibt Klassendaten
if (c==A.class) super.writeObject(A.s); } }
class MyObjectInputStream extends ObjectInputStream 
{
  public MyObjectInputStream(InputStream in) throws 
                    IOException, StreamCorruptedException  { 
    super(in); 
  }

Subklasse von ObjectInputStream mit
resolveClass()

  protected Class resolveClass(ObjectStreamClass v)
        throws IOException, ClassNotFoundException   {
    // resolveClass()in ObjectInputStream 
holt Klassen-Objekt
Class c= super.resolveClass(v);
    // hinter der Klassen-Info werden die Daten übertragen
if (c== A.class) A.s= (String) super.readObject(); return c; } }

Vor dem Serialisieren des ersten Objekts der Klasse A werden auch die Klassendaten übertragen. Hierzu zwei kleine Code-Fragemente:

ObjectOutput oout= new MyObjectOutputStream(out);
A.s= "Klassendaten";
oout.writeObject(new A('a')); 
A.s="";

Umgekehrt wird A.s mit dem ersten Deserialisieren wieder gesetzt:

ObjectInput oin= new MyObjectInputStream(in);
oin.readObject(); 
System.out.println(A.s);                   // :: Klassendaten

Galileo Computing

12.6.2 replaceObject() und resolveObject()  downtop

Steht man vor dem Problem, Objekte serialisieren zu müssen, die weder Serializable noch Externalizable implementiert haben, so kann man diese durch serialisierbare Repräsentanten ersetzen.

Hierzu definiert man Subklassen der Object-Streams, deren Methoden

   protected Object replaceObject(Object o) throws IOException;
   protected Object resolveObject(Object o) throws IOException;

Icon

man überschreiben muss.

Restriktionen zum Replace/Resolve-Mechanismus

Es sind folgende Restriktionen einzuhalten:

gp  In den Subklassen von ObjectOutputStream bzw. ObjectInputStream müssen vorher die Methoden enableReplaceObject(true) bzw. enableResolveObject(true) aufgerufen werden.
gp  Ist ein Sicherheitsmanager installiert, wird die Erlaubnis überprüft16  und – sofern nicht vorhanden – eine SecurityException ausgelöst.
gp  Die Methode replaceObject() ersetzt das Originalobjekt durch einen serialisierbaren Repräsentanten.
gp  Die Methode resolveObject() ersetzt den Repräsentanten wieder durch ein Objekt der ursprünglichen Klasse.

Damit keine heillose Konfusion entstehen kann, gilt zusätzlich:

gp  Für Objekte der Klassen Class und ObjectStreamClass werden die Methoden replaceObject() bzw. resolveObject() nicht aufgerufen.

Beispiel

Die erste Aufgabe besteht darin, längere Strings komprimiert zu übertragen.

String-Replacement:
Komprimierung mittels Zips

Hat ein String unter 100 Zeichen oder kann er nur zu weniger als 50% komprimiert werden, wird er normal übertragen. Ansonsten wird er als komprimierte Instanz von SerString im Stream übertragen.

Die Klasse SerString benutzt hierzu die Klassen Deflater und Inflater des Packages java.util.zip.

Die String-Prüfung und den eventuellen Austausch gegen eine komprimierte SerString-Instanz übernimmt die Factory-Klasse SerString selbst:

Eine Replacement-Klasse für String

class SerString implements Serializable {
  private byte[] b;
  private int slen;
  // Instanzen können nur durch tryCompress 
angelegt werden
private SerString(byte[] cs, int cslen, int len) { slen= len; b= new byte[cslen]; System.arraycopy(cs,0,b,0,cslen); }
  // liefert eine SerString-Instanz oder 
denselben String  
  public static Object 
tryCompress(String s) {
    if (s.length()>99) {
      byte[] cs;
      int slen, i;
      Deflater d= new Deflater(Deflater.BEST_COMPRESSION);
      try {
        // Byte-Array zum Komprimieren übergeben
d.setInput(cs= s.getBytes("UTF-8")); slen= cs.length; cs= new byte[slen/2]; // mindestens 50 % Kompression d.finish(); // Ende mitteilen i=d.deflate(cs); // komprimieren if (d.finished()) // sofern Array-Größe ausreicht return new SerString(cs,i,slen); } catch (Exception e) {}; } return s; // ansonsten String zurück }
 // dekomprimiert wieder zum String 

 public String toString() { 
    int len;
    byte[] sb= new byte[slen]; // Größe des UTF8-Arrays
    Inflater infl= new Inflater();
    try {
      infl.setInput(b);        // Byte-Array setzen
      len= infl.inflate(sb);   // dekomprimieren
      return new String(sb,0,len,"UTF-8"); // String zurück
    } catch (Exception e) {};
    return null;               // bei Fehler null
  }
}

Die zweite Aufgabe besteht darin, mit Hilfe einer Wrapper-Klasse von beliebigen nicht serialisierbaren Objekten zumindest Default-Objekte derselben Klasse deserialisieren zu können.

Wrapper für nicht serialisierbare Objekte

class SerWrapper implements Serializable {
  private Class c;
 // jedes nicht serialisierbare Objekt wird
// durch sein Class-Objekt ersetzt
public SerWrapper (Object o) { c= o.getClass(); }
  // Konstruktion eines neuen (Default-)Objekts 
der Klasse
public Object get() { try { // setzt No-Arg-Konstruktor voraus! return c.newInstance(); } catch (Exception e) { return null; } } }

In der Subklasse von ObjectOutputStream muss die Replace-Operation nun String-Objekte und nicht serialisierbare Objekte abfangen und durch Repräsentanten der beiden o.a. Klassen ersetzen:

Subclassing von
ObjectOutputStream

class MyObjectOutputStream extends ObjectOutputStream {

  public MyObjectOutputStream(OutputStream out)
                     throws IOException, SecurityException {
    // Stream an ObjectOutputStream weiterreichen
super(out); enableReplaceObject(true); // siehe 1. Regel oben }

Methode replaceObject()

  protected Object replaceObject(Object obj) 
                                        throws IOException {
    if (obj instanceof String)
      return SerString.tryCompress((String)obj);
    else if (obj instanceof Serializable)
      return obj;
    else
      return new SerWrapper(obj);
  }
}

In der Subklasse von ObjectInputStream führt die Resolve-Operation die zu replace inverse Operation aus:

Subclassing von
ObjectInputStream

class MyObjectInputStream extends ObjectInputStream {
  public MyObjectInputStream(InputStream in)
               throws IOException, SecurityException  {
    super(in);
    enableResolveObject(true);
  }

Methode
resolveObject()

  protected Object resolveObject(Object obj) throws IOException {
    if (obj instanceof SerString) 
      return ((SerString)obj).toString();
    else if (obj instanceof SerWrapper) 
      return ((SerWrapper)obj).get();
    else return obj;
  }
}

Zum Test können zwei triviale Klassen verwendet werden.

class Any { public String toString() { return "Any"; 
} }
class SW implements Serializable {
  String s;          // wird auch durch SerString untersucht
  public SW(String s) { this.s= s; }
  public String toString() { return s; }
}

Replacement erfolgt rekursiv

Ein Test-Code zur Objekt-Übertragung ist analog zur Default-Serialisierung:

public class Test {
 public static void main(String[] args) {
  String s= "dies soll ein langer komprimierbarer String sein...";
  ByteArrayOutputStream baos= new ByteArrayOutputStream();
  try {
    ObjectOutput oout= new MyObjectOutputStream(baos);
    oout.writeObject(new Any()); // wird ersetzt durch SerWrapper
    oout.writeObject(new SW(s)); // String-Feld wird komprimiert
    ObjectInput oin= new MyObjectInputStream(
                   new ByteArrayInputStream(baos.toByteArray()));
      System.out.println(oin.readObject());
      System.out.println(oin.readObject());
    } catch (Exception e) { System.out.println(e); }
  }
}
gp  Interessant ist, dass Replacements auch für Felder rekursiv erfolgen.

Galileo Computing

12.6.3 writeObjectOverride() und readObjectOverride()  downtop

Override-Methoden:
komplette Übernahme des Streams/Protokolls

Die letzte hier angesprochene Möglichkeit ist keine Anpassung der Default-Serialisierung, denn es gibt keine vordefinierten Sequenzen. Der Stream ist leer, jedes Byte muss selbst geschrieben werden.

gp  Die Override-Methoden definieren ein komplett neues Protokoll.
gp  Es gibt nicht wie bei Externalizable ein Protokoll, in das man seine Objekt-Informationen einbetten kann.

Diese radikale Übernahme erreicht man durch Überschreiben von17 

   protected void writeObjectOverride 
(Object obj) 
                                         throws IOException;
   protected Object readObjectOverride() 
throws 
     OptionalDataException,ClassNotFoundException,IOException;

Diese beiden Methoden sind in den Object-Streams implementiert, aber ohne Funktion. Für eine eigene Implementierung gelten folgende Restriktionen:

Icon
Restriktionen zu read-/writeObjectOverride()

Die Override-Methoden

gp  können nur in Subklassen der Object-Streams überschrieben werden, die den No-Arg-Konstruktor der Object-Streams aufrufen.
gp  können Objekte aller Klassen übertragen, auch solche, die nicht als serialisierbar gekennzeichnet sind.
gp  müssen das gesamte Übertragungs-Protokoll selbst realisieren.
gp  haben keinen Zugriff auf private deklarierte Felder der übertragenen Objekte.
gp  werden von einem Sicherheitsmanager – sofern installiert – im No-Arg-Konstruktor geprüft.18 

Der No-Arg-Konstruktor setzt ein private deklariertes Flag, das den Default-Methoden signalisiert, nur ihre zugehörigen Override-Methoden auszuführen.19 

Da der No-Arg-Konstruktor keinen Outputstream bzw. Inputstream entgegennimmt, werden keine Felder der Object-Streams gesetzt.

Beispiel

Definition eines eigenen Protokolls

Das Beispiel zeigt nur die prinzipiellen Schritte, da ein eigenes Protokoll verständlicherweise umfangreich und applikationsabhängig ist.

class MyObjectOutputStream extends ObjectOutputStream 
{
  private OutputStream out;
  public MyObjectOutputStream(OutputStream out)
              throws IOException, SecurityException {
   // nur super(); erlaubt, was man sich schenken kann
// mit out muss man alles selbst in die Hand nehmen
this.out= out; }

Methode writeObjectOverride()

  protected void writeObjectOverride(Object obj) 
                                 throws IOException {
    // ein Trivial-Protokoll als Demo
    String s= obj.getClass().getName();     // Name der Klasse
    out.write(s.length()>>8);               // Länge in 2 
Byte
    out.write(s.length());
    out.write(s.getBytes());                // rein ASCII
  }
}
class MyObjectInputStream extends ObjectInputStream 
{
  private InputStream in;
  public MyObjectInputStream(InputStream in)
         throws IOException, SecurityException {
    this.in= in;
  }

Methode readObjectOverride()

  protected Object readObjectOverride() 
                       throws OptionalDataException, 
                 ClassNotFoundException, IOException {
    int i= in.read()<<8; i+= in.read();     // Länge lesen
    byte[] b= new byte[i];
    in.read(b);
    // Klasse mit dem gelesenen Namen laden
    Class c= Class.forName(new String(b));
    try {
      return c.newInstance();               // Instanz liefern
    } catch (Exception e) { return null; }; // ansonsten null
  }
}

Zum Test wird die bereits im letzten Beispiel verwendete Klasse Any mit Default-Konstruktor benötigt.

class Any {
  public String toString() { return "Any"; }
}

Test der Override-Methoden

Der Stream mit eigenem Trivial-Protokoll wird mit Sniffer dargestellt.

public class Test {
  public static void main(String[] args) {
    byte[] barr= null;
    ByteArrayOutputStream baos= new ByteArrayOutputStream();
    try {
      ObjectOutput oout= new MyObjectOutputStream(baos);
      oout.writeObject(new Any());
      barr= baos.toByteArray();
      System.out.println(Sniffer.toHexAsciiString(barr,16));
      ObjectInput oin= new MyObjectInputStream(
                         new ByteArrayInputStream(
                                       baos.toByteArray()));
      System.out.println(oin.readObject());
    } catch (Exception e) { System.out.println(e); }
  }
}
00 09 6b 61 70 31 32 2e 41 6e 79                
..kap12.Any
Any

Eigenes Protokoll?
Effizienz, Hardware ...

Die Realisierung eines eigenen Protokolls ist – wie zu sehen – aufwändig und lohnt nur bei speziellen Anwendungen, wie z.B. für die effiziente Übertragung von großen Arrays einer Klasse.


Galileo Computing

12.7 Zusammenfassung  downtop

Die Idee der Serialisierung ist faszinierend, da sie das Marshaling bzw. Unmarshaling von beliebigen Objekten – in der Regel typsicher – erlaubt.

Mit den Interfaces Serializable und Externalizable werden die grundlegenden Mechanismen und Protokolle vorgestellt und anhand einer Sniffer-Klasse demonstriert.

Externalizable ist das Mittel der Wahl, um Felder und Felddaten von Objekten beliebig manipulieren zu können. Abgesehen von allgemeinen Header- und Klassen-Informationen, ist das Stream-Protokoll von Externalizable frei bestimmbar.

Neben eigener Traversierung des Objekt-Graphen ist ein gravierender Nachteil von Externalizable, dass die beiden Methoden public zu implementieren sind.

Die Default-Serialisierung durch Implementation von Serializable bietet automatisches Traversieren und Übertragen aller erreichbaren Objekte inklusive der Superklassen-Felder eines Objekts. Es sind jedoch gewisse Regeln zu beachten, die vorgestellt werden.

Beginnend mit sehr einfachen Anpassungen wie transient deklarierten Feldern kann die Default-Serialisierung sukzessive durch eigene Funktionalität ergänzt oder ersetzt werden. Hierzu stehen je nach Anforderung sehr verschiedene Mechanismen bereit, die im Einzelnen besprochen und anhand von Beispielen demonstriert werden.

Sehr interessant ist das Broker-Konzept, abgeleitet vom Broker-Pattern, das es erlaubt, die Serialisierung als eigenen unabhängigen Dienst aus der Klasse auszulagern. Diese Separation birgt einige Vorteile.

Da die Default-Serialisierung nur Objekte überträgt, stehen insbesondere für statische Daten zwei zusätzliche Methoden bereit.

Für tief gehende Anpassungen müssen die Object-Streams selbst abgeleitet werden. Die Subklassen können dann so angepasst werden, dass sie sogar Objekte übertragen können, die ansonsten nicht serialisierbar sind.

In letzter Konsequenz kann ein komplett eigenes Protokoll realisiert werden.

Das »Serialisierungs-Gebäude« ist in kurzer Zeit stark erweitert worden, was sich leider im Code der Packages und in teilweise konkurrierenden Konzepten widerspiegelt.

Die Mechanismen laufen teilweise »magic« ab, teilweise beachten sie strikt die normalen Java-Sprachregeln.


Galileo Computing

12.8 Testfragen20    toptop

Zu jeder Frage können jeweils eine oder mehrere Antworten bzw. Aussagen richtig sein.

1. Welche Aussagen sind richtig?

A: Serializable und Externalizable sind Marker-Interfaces.

B: Subklassen einer externalizable Klasse können Serializable implementieren, damit ihre Objekte wieder per Default-Serialisierung übertragen werden.

C: Bei einer externalizable Klasse müssen alle Felder, die übertragen werden sollen, explizit codiert werden.

D: Bei einer externalizable (Sub-)Klasse müssen alle Felder von einer Superklasse, die übertragen werden sollen, explizit codiert werden.

E: Bei der Default-Serialisierung werden keine Daten statischer Felder übertragen.

F: transient deklarierte Felder können auch von externalizable Klassen nicht übertragen werden.

G: Mit gleichen Werten des serialVersionUID werden verschiedene Versionen von Klassen als stream-kompatibel erklärt.

H: Nur wenn der Wert des serialVersionUID in verschiedene Klassen-Versionen gleich ist, sind sie stream-kompatibel.

I: Neue Klassen-Versionen sind nur dann stream-kompatibel zur alten, wenn sie alle Instanz-Felder der alten Version auch enthalten.

J: Die Default-Serialierung überträgt mit einer Instanz automatisch nur die Objekte, die von einem Instanz-Feld referenziert werden und serialisierbar sind.

K: Mit den Object-Streams können nur Objekte, aber keine einzelnen Daten von primitiven Typen (außerhalb von Objekten) übertragen werden.

L: Mit den Object-Streams können einzelne Daten von primitiven Typen typsicher übertragen werden, d.h., beim Deserialisieren wird eine Ausnahme ausgelöst, wenn der falsche primitive Typ gewählt wird.

2. Welche Aussagen sind zu folgenden Klassen richtig?

class Int { int i= 1; }
class S implements Serializable {
  private transient Int io;                         ¨
  //private Int io;                                 ¦
  public S(int i) { io= new Int(); io.i= i; }
}

A: Objekte der Klasse S können mit Hilfe der Default-Serialisierung übertragen werden.

B: Das Feld io eines deserialisierten Objekts von S hat immer den Wert null.

C: Beim Deserialisieren wird das Feld io eines Objekts von S erzeugt und der Wert von io.i auf 1 gesetzt.

D: Wird Zeile ¨ durch Zeile ¦ ersetzt, können Objekte der Klasse S nicht mehr mit Hilfe der Default-Serialisierung übertragen werden.

3. Welche Aussagen sind zu folgenden Klassen richtig?

abstract class Ex implements Externalizable {
  int i;
}
class ExSub extends Ex implements Serializable { 
           ¨
  String s;
}

A: Die Klasse Ex wird fehlerfrei kompiliert.

B: Die Klasse ExSub wird fehlerfrei kompiliert.

C: Die Klasse ExSub muss die beiden Methoden readExternal() und writeExternal() implementieren.

D: Die Angabe implements Serializable in Zeile ¨ ist überflüssig.

4. Welche Aussagen sind zu folgender Applikation richtig?

class SInt implements Serializable { 
  int i= 1; 
}
class S implements Serializable {
  private SInt i1;
  private transient int i2;
  public S(int i) { 
    i1= new SInt(); 
    i1.i= i; this.i2= i; 
  }
  public String toString() { return i1.i + ","+i2; }
}
public class Test {
  public static void main(String[] args) {
    try {
      ObjectOutput oout= new ObjectOutputStream(
                           new FileOutputStream("test.dat"));
      oout.writeObject(new S(10)); 
      oout.close();
      ObjectInput oin= new ObjectInputStream(
                         new FileInputStream("test.dat"));
      System.out.println(oin.readObject());                    ¨
    } catch (Exception e) { System.out.println(e); }
  }
}

A: Die Ausgabe ist: 10,10

B: Die Ausgabe ist: 10,0

C: Die Ausgabe ist: 10,1

D: Die Ausführung von Test führt zu einer Ausnahme in Zeile ¨.

5. Welche Aussagen sind zu folgender Applikation richtig?

class Base {
  String s1= "Base";
}
class SerD extends Base implements Serializable 
{
  String s2= "SerD";
  Base b;
  public SerD() { b= new Base(); }              
      ¨
//public SerD() {}                                    ¦
  public String toString() { return s1+s2+b; }
}
public class Testfragen {
  public static void main(String[] args) {
    try {
      ObjectOutput oout= new ObjectOutputStream(
                           new FileOutputStream("test.dat"));
      oout.writeObject(new SerD());
      oout.close();
      ObjectInput oin= new ObjectInputStream(
                         new FileInputStream("test.dat"));
      System.out.println(oin.readObject());
    } catch (Exception e) { System.out.println(e); }
  }
}

A: Die Ausführung von Test führt zu einer Ausnahme.

B: Die Ausgabe ist: BaseSerDnull

C: Wird Zeile ¨ weggelassen, wird Test fehlerfrei ausgeführt.

D: Wird Zeile ¨ durch Zeile ¦ ersetzt, ist die Ausgabe: BaseSerDnull






1    <X> steht für die Bezeichnung des primitiven Typs, z.B. writeInt().

2    Konstruktor ohne Argumente

3    Wer also unbedingt darauf besteht, seine Packages und Klassen mit Nicht-ASCII-Zeichen, z.B. deutschen Umlauten, zu benennen, hat spätestens bei der Serialisierung in Netzen viel Freude an seiner Wahl.

4    Im Unterschied zum deutschen Begriff »serialisierbar«, der offen lässt, ob die Klasse Serializable oder Externalizable implementiert.

5    Sollte der Konstruktor nicht im Zugriff stehen, kommt es beim Deserialisieren zu einer InvalidClassException.

6    Details werden hier weggelassen, da sie für den prinzipiellen Ablauf uninteressant sind. Sie können aber im Object Serialization ReleaseStream Protokol von Sun nachgelesen werden.

7    Zu speziellen Serialisierungs-Methoden siehe Abschnitte ab Einfache Anpassungen von Serializable in diesem Kapitel.

8    Zum Beispiel dann, wenn man ein Feld zu einer Klasse hinzufügen muss, zu der bereits serialisierte Objekte existieren, die auch gelesen werden müssen.

9    Sie werden in keinem Interface erklärt, von der JVM automatisch aufgerufen und ersetzen ihre gleichnamigen Methoden in den Object-Streams.

10    Die Lösung ist allerdings höchst ineffizient, da in jedem Objekt nun statische Werte übertragen werden (siehe 12.6.1).

11    writeReplace() kann dazu auch private deklariert sein.

12    SHA: Secure Hash Algorithm.

13    Das UID hat keine Bedeutung für externalizable Klassen, da diese ohnehin ihr eigenes Protokoll zur Objekt-Übertragung definieren müssen.

14    id steht für den Hashcode, L kennzeichnet long.

15    Siehe JDK

16    Die Überprüfung erfolgt mit secManager.checkPermission(SUBSTITUTION_PERMISSION), wobei die Konstante static final SerializablePermission SUBSTITUTION_PERMISSION in ObjectStreamConstants definiert ist.

17    Dies ist möglich, denn writeObject() und readObject() sind final deklariert!

18    Die Prüfung erfolgt mit sm.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION)

19    Diese Art der Methoden-Delegation ist ein Überschreiben durch die Hintertür mit Sicherheitscheck.

20    Es werden nur Testfragen zum Basiswissen gestellt, die für die Zertifizierung
relevant sind.

  

Perl – Der Einstieg




Copyright © Galileo Press GmbH 2001 - 2002
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken und speichern. Ansonsten unterliegt das <openbook> denselben Bestimmungen wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.
Die Veröffentlichung der Inhalte oder Teilen davon bedarf der ausdrücklichen schriftlichen Genehmigung von Galileo Press. Falls Sie Interesse daran haben sollten, die Inhalte auf Ihrer Website oder einer CD anzubieten, melden Sie sich bitte bei: stefan.krumbiegel@galileo-press.de


[Galileo Computing]

Galileo Press GmbH, Gartenstraße 24, 53229 Bonn, fon: 0228.42150.0, fax 0228.42150.77, info@galileo-press.de