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.
12.1 Serialisierung: Kommunikation auf Basis von Objekten
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.
|
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
|
writeObject(), das ein Objekt in einen Byte-Stream umwandelt. |
|
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.
12.2 Grundlagen der Serialisierung
Allen Protokollen und Mechanismen der Serialisierung liegt eine Prämisse zugrunde:
Objekt-Rekonstruktion aus einer Byte-Sequenz
|
Der Empfänger muss anhand des erhaltenen Byte-Streams in der Lage sein, die ursprünglichen Objekte zu rekonstruieren. |
|
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 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:
|
Klassen, deren Objekte serialisiert werden sollen, implementieren Serializable bzw. Externalizable. |
|
Klassen, die die eigentliche Kommunikation vornehmen, implementieren die Interfaces ObjectOutput bzw. ObjectInput. |
12.2.1 Standard-Serialisierung
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
|
Um ein Objekt zu serialisieren, wird es der Instanz-Methode writeObject() von ObjectOutputStream übergeben. |
readObject() deserialisiert
|
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 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
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>()1 und read<X>() der Interfaces DataOutput und DataInput auch primitive Typen übertragen werden (siehe Abb. 12.1 und 12.2).
12.3 Übertragungs-Protokolle
Selbst wenn nur Daten primitiver Typen übertragen werden, beruht die Serialisierung auf einem vereinbarten Protokoll.
Serialisierungs-Protokoll mit Header
|
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).
12.3.1 Protokoll zu primitiven Typen
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
|
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:
|
Sie lässt die Analyse der Struktur des Byte-Streams durch die Sniffer-Klasse sehr einfach zu. |
|
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
|
Grundsätzlich werden alle Zeichen vom Typ char oder String als UTF8 im Stream codiert. |
12.3.2 Protokolle zur Objekt-Serialisierung
Für Objekte ist das Protokoll sowie das Stream-Format verständlicherweise wesentlich komplexer.
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 |
|
|
public void writeExternal(ObjectOutput out) |
|
|
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 |
|
|
static deklarierte Klassen-Variablen nicht serialisiert werden |
|
|
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
|
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.
12.3.3 Protokoll zu Externalizable
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
|
Externalizable mit den beiden Methoden writeExternal() bzw. readExternal() implementieren. |
|
einen public deklarierten No-Arg-Konstruktor 2 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
|
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.
12.3.4 Protokoll zu Serializable
Implementiert eine Klasse Serializable, so kann die Serialisierung völlig transparent von der JVM übernommen werden.
Allerdings stellt das Protokoll gewisse Anforderungen an die Objekte und ihre Klassen.
1. Anforderungen an eine Klasse
Anforderungen an serializable Klassen
Eine Klasse ist serializable4 , wenn
|
keine ihrer Superklassen Externalizable implementiert und |
|
sie Zugriff auf den No-Arg-Konstruktor der ersten Superklasse hat, die nicht serialisierbar ist.5 |
Anforderungen an serializable Objekte
2. Anforderungen an ein Objekt
Ein Objekt ist dann serializable, wenn
|
seine Klasse serializable ist und |
|
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.
|
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 ab6 :
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 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. |
|
|
Für externalizable Objekte wird der public No-Arg-Konstruktor aufgerufen und anschließend readExternal(). |
|
|
Fü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. |
|
|
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
|
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]
}
}
12.4 Einfache Anpassungen von Serializable
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.
12.4.1 serialPersistentFields: Ersatz für transient
serialPersistentFields überschreibt transient
Die Kennzeichnung der nicht serialisierbaren Instanz-Felder als transient ist zwar einfach, aber manchmal nicht flexibel genug.8
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
|
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)
};
}
12.4.2 Externalizable: Kapselung unmöglich
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.
|
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.
12.4.3 Klasseninterne Anpassung der Default-Serialisierung
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.9
|
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.
|
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.
12.4.4 Broker-Pattern: Stream-Ersatzobjekte
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.
|
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.
|
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 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
|
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.
|
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);
}
}
12.5 Klassen-Evolution
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
|
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. |
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
|
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).
|
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.
12.5.1 Stream-Kompatibilität
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
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
|
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!). |
|
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:
|
Typ-Wechsel: Ein Feld mit gleichem Namen wechselt den Typ. |
|
Ä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.
12.6 Anpassungen der Object-Streams
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:
|
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.
12.6.1 annotateClass() und resolveClass() für Klassendaten
Übertragen von einmaligen Klassen-Informationen
Statische Felder werden bereits beim Laden der Klasse angelegt und initialisiert, d.h. vor jeder Objekt-Erzeugung bzw. -Übertragung.
|
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
12.6.2 replaceObject() und resolveObject()
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;
man überschreiben muss.
Restriktionen zum Replace/Resolve-Mechanismus
Es sind folgende Restriktionen einzuhalten:
|
In den Subklassen von ObjectOutputStream bzw. ObjectInputStream müssen vorher die Methoden enableReplaceObject(true) bzw. enableResolveObject(true) aufgerufen werden. |
|
Ist ein Sicherheitsmanager installiert, wird die Erlaubnis überprüft16 und – sofern nicht vorhanden – eine SecurityException ausgelöst. |
|
Die Methode replaceObject() ersetzt das Originalobjekt durch einen serialisierbaren Repräsentanten. |
|
Die Methode resolveObject() ersetzt den Repräsentanten wieder durch ein Objekt der ursprünglichen Klasse. |
Damit keine heillose Konfusion entstehen kann, gilt zusätzlich:
|
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); }
}
}
|
Interessant ist, dass Replacements auch für Felder rekursiv erfolgen. |
12.6.3 writeObjectOverride() und readObjectOverride()
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.
|
Die Override-Methoden definieren ein komplett neues Protokoll. |
|
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:
Restriktionen zu read-/writeObjectOverride()
Die Override-Methoden
|
können nur in Subklassen der Object-Streams überschrieben werden, die den No-Arg-Konstruktor der Object-Streams aufrufen. |
|
können Objekte aller Klassen übertragen, auch solche, die nicht als serialisierbar gekennzeichnet sind. |
|
müssen das gesamte Übertragungs-Protokoll selbst realisieren. |
|
haben keinen Zugriff auf private deklarierte Felder der übertragenen Objekte. |
|
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.
12.7 Zusammenfassung
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.
12.8 Testfragen20
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.
|