Galileo Computing < openbook > Galileo Computing - Professionelle Bücher. Auch für Einsteiger.
Professionelle Bücher. Auch für Einsteiger

 << zurück
Java ist auch eine Insel von Christian Ullenboom
Programmieren für die Java 2-Plattform in der Version 5
Java ist auch eine Insel

Java ist auch eine Insel
5., akt. und erw. Auflage
1454 S., mit CD, 49,90 Euro
Galileo Computing
ISBN 3-89842-747-1
gp Kapitel 12 Datenströme und Dateien
  gp 12.1 Datei und Verzeichnis
    gp 12.1.1 Dateien und Verzeichnisse mit der Klasse File
    gp 12.1.2 Dateieigenschaften und -attribute
    gp 12.1.3 Änderungsdatum einer Datei, Nur-Lese-Rechte setzen
    gp 12.1.4 Dateien berühren, neue Dateien anlegen, temporäre Dateien
    gp 12.1.5 Umbenennen und Verzeichnisse anlegen
    gp 12.1.6 Die Wurzel aller Verzeichnisse/Laufwerke
    gp 12.1.7 Verzeichnisse listen und Dateien filtern
    gp 12.1.8 Dateien und Verzeichnisse löschen
    gp 12.1.9 Verzeichnisse nach Dateien rekursiv durchsuchen
    gp 12.1.10 Namen der Laufwerke
    gp 12.1.11 URL- und URI-Objekte aus einem File-Objekt ableiten
    gp 12.1.12 Locking
    gp 12.1.13 Sicherheitsprüfung
    gp 12.1.14 Implementierungsmöglichkeiten für die Klasse File
    gp 12.1.15 Mime-Typen
  gp 12.2 Dateien mit wahlfreiem Zugriff
    gp 12.2.1 Ein RandomAccessFile zum Lesen und Schreiben öffnen
    gp 12.2.2 Aus dem RandomAccessFile lesen
    gp 12.2.3 Schreiben
    gp 12.2.4 Die Länge des RandomAccessFile
    gp 12.2.5 Hin und her in der Datei
    gp 12.2.6 Wahlfreier Zugriff und Pufferung mit Unified I/O
  gp 12.3 Stream-Klassen und Reader/Writer
    gp 12.3.1 Die abstrakten Basisklassen
    gp 12.3.2 Übersicht über Ein-/Ausgabeklassen
  gp 12.4 Binäre Ein-/Ausgabe-Klassen InputStream/OutputStream
    gp 12.4.1 Die abstrakte Basisklasse OutputStream
    gp 12.4.2 Die Schnittstellen Closeable und Flushable
    gp 12.4.3 Ein Datenschlucker
    gp 12.4.4 Anwendung der Klasse FileOutputStream
    gp 12.4.5 Die abstrakte Eingabeklasse InputStream
    gp 12.4.6 Ressourcen wie Grafiken aus dem Klassenpfad und aus Jar–Archiven laden
    gp 12.4.7 Anwenden der Klasse FileInputStream
    gp 12.4.8 Kopieren von Dateien
    gp 12.4.9 Das FileDescriptor-Objekt
  gp 12.5 PrintStream und Konsolenausgaben
    gp 12.5.1 Die Klasse PrintStream
    gp 12.5.2 Die Schnittstelle Appendable
    gp 12.5.3 System.in und System.out
    gp 12.5.4 Ströme umlenken
    gp 12.5.5 Den Bildschirm löschen und Textausgaben optisch aufwerten
  gp 12.6 Daten filtern durch FilterInputStream und FilterOutputStream
    gp 12.6.1 DataOutputStream/DataInputStream
  gp 12.7 Besondere OutputStream- und InputStream-Klassen
    gp 12.7.1 Mit ByteArrayOutputStream in ein Byte-Feld schreiben
    gp 12.7.2 Mit ByteArrayInputStream aus einem Byte-Feld lesen
    gp 12.7.3 Ströme zusammensetzen mit SequenceInputStream
  gp 12.8 Die Unterklassen von Writer
    gp 12.8.1 Die abstrakte Basisklasse Writer
    gp 12.8.2 Ausgabemöglichkeiten durch PrintWriter erweitern
    gp 12.8.3 Datenkonvertierung durch den OutputStreamWriter
    gp 12.8.4 In Dateien schreiben mit der Klasse FileWriter
    gp 12.8.5 StringWriter und CharArrayWriter
    gp 12.8.6 Writer als Filter verketten
    gp 12.8.7 Gepufferte Ausgabe durch BufferedWriter
    gp 12.8.8 Daten mit FilterWriter filtern
  gp 12.9 Die Klassen um Reader
    gp 12.9.1 Die abstrakte Basisklasse Reader
    gp 12.9.2 Automatische Konvertierungen mit dem InputStreamReader
    gp 12.9.3 Dateien lesen mit der Klasse FileReader
    gp 12.9.4 StringReader und CharArrayReader
  gp 12.10 Die Filter für Zeichenströme
    gp 12.10.1 Gepufferte Eingaben mit der Klasse BufferedReader
    gp 12.10.2 LineNumberReader zählt automatisch Zeilen mit
    gp 12.10.3 Eingaben filtern mit der Klasse FilterReader
    gp 12.10.4 Daten mit der Klasse PushbackReader zurücklegen
  gp 12.11 Kommunikation zwischen Threads mit Pipes
    gp 12.11.1 PipedOutputStream und PipedInputStream
    gp 12.11.2 PipedWriter und PipedReader
  gp 12.12 Datenkompression
    gp 12.12.1 Java-Unterstützung beim Komprimieren und Zusammenpacken
    gp 12.12.2 Datenströme komprimieren
    gp 12.12.3 Zip-Archive
    gp 12.12.4 Jar-Archive
  gp 12.13 Prüfsummen
    gp 12.13.1 Die Schnittstelle Checksum
    gp 12.13.2 Die Klasse CRC32
    gp 12.13.3 Die Adler32-Klasse
  gp 12.14 Persistente Objekte und Serialisierung
    gp 12.14.1 Objekte speichern mit der Standard-Serialisierung
    gp 12.14.2 Objekte über die Standard-Serialisierung lesen
    gp 12.14.3 Die Schnittstelle Serializable
    gp 12.14.4 Nicht serialisierbare Attribute mit transient aussparen
    gp 12.14.5 Das Abspeichern selbst in die Hand nehmen
    gp 12.14.6 Tiefe Objektkopien
    gp 12.14.7 Versionenverwaltung und die SUID
    gp 12.14.8 Wie die ArrayList serialisiert
    gp 12.14.9 Probleme mit der Serialisierung
    gp 12.14.10 Serialisieren in XML-Dateien
    gp 12.14.11 JavaBeans Persistence
    gp 12.14.12 XStream
  gp 12.15 Zugriff auf SMB-Server
    gp 12.15.1 jCIFS
  gp 12.16 Tokenizer
    gp 12.16.1 StreamTokenizer
    gp 12.16.2 CSV (Comma Separated Values)-Dateien verarbeiten
  gp 12.17 Die Logging-API


Galileo Computing

12.14 Persistente Objekte und Serialisierundowntop

Objekte liegen zwar immer nur zur Laufzeit vor, doch auch nach dem Beenden der virtuellen Maschine soll ihre Struktur nicht verloren gehen. Gewünscht ist ein Mechanismus, der die Objektstruktur und Variablenbelegung zu einer bestimmten Zeit sicher (persistent) macht und an anderer Stelle wieder hervorholt und die Objektstruktur und Variablenbelegung restauriert. Im gespeicherten Datenformat müssen alle Informationen wie Objekttyp und Variablentyp enthalten sein, um später das richtige Wiederherstellen zu ermöglichen. Da Objekte oftmals weitere Objekte einschließen, müssen auch diese Unterobjekte gesichert werden. (Schreibe ich eine Menüzeile, so ist sie ohne die Menüeinträge wertlos. Auch eine Datenstruktur ist ohne die referenzierten Objekte sinnlos.) Genau dieser Mechanismus wird auch dann angewendet, wenn Objekte über das Netzwerk schwirren. Die persistenten Objekte sichern also neben ihren eigenen Informationen auch die Unterobjekte – also die von der betrachtenden Stelle aus erreichbaren. Beim Speichern wird rekursiv ein Objektbaum durchlaufen, um eine vollständige Datenstruktur zu erhalten. Der doppelte Zugriff auf ein Objekt wird dabei ebenso beachtet wie der Fall, dass zyklische Abhängigkeiten auftreten. Jedes Objekt bekommt dabei ein Handle, sodass es im Datenstrom nur einmal kodiert wird.

Unter Java lassen sich Objekte über verschiedene Ansätze persistent speichern:

gp  Standardserialisierung. Der Punkt, mit dem wir uns im Folgenden beschäftigen wollen. Die Objektstruktur und Zustände werden in einem binären Format gesichert. Sie wird auch Java Object Serialization (JOS) genannt.
gp  Serialisierung in XML. Java-Beans – und nur solche – können wir in einem XML-Format sichern. (Auch JavaBeans Persistence (JBP) genannt.)
gp  Datenbanken. Die Daten schreiben wir von Hand über JDBC in die Datenbank.

Diese drei Techniken sind mit Standard-Java zu lösen. Die nächsten Implementierungen bauen auf zusätzlichen Frameworks auf:

gp  Bean-Persistenz durch EJB. Die aufwändigste Lösung. Spezielle Beans, die Entity-Beans, werden automatisch in die Datenbank geschrieben. EJBs sind Teil der Java EE, Java 2 Enterprise Edition.
gp  Objekt-Relationales Mapping. Das Schreiben und Lesen von Hand über JDBC ist sehr lästig, so dass dieser Schritt automatisiert werden muss. Über eine Beschreibung der Objekt-Daten ist es möglich, die Daten automatisch auf Tabellen einer Datenbank abzubilden. Umgesetzt zum Beispiel durch Hibernate oder JDO (Java Data Objects), eine immer populärer werdende Möglichkeit, Objekte in Datenbanken oder anderen Containern abzulegen und auszulesen.
gp  JAXB. Abbilden der Objektstruktur auf XML-Dokumente. Die Struktur der XML-Datei ist über Schema beschrieben. Anders als die XML-Standardserialisierung ist eine Schema-Datei dringend nötig, und die Daten müssen auch keine Beans sein. JAXB wird vermutlich Teil von Java 6.

Galileo Computing

12.14.1 Objekte speichern mit der Standard-Serialisierung  downtop

Die Standard-Serialisierung ist eine der einfachsten Möglichkeiten, Objekte persistent zu machen. Dabei werden die Objektzustände in einen Byte-Strom geschrieben. Im Zentrum steht die Klasse ObjectOutputStream und die Methode writeObject(). Während der Serialisierung geht ObjectOutputStream die Zustände und Objektverweise rekursiv ab und schreibt die Zustände Schritt für Schritt in einen OutputStream.

An einem Beispiel lässt sich gut erkennen, wie ein ObjectOutputStream einen String und das aktuelle Tagesdatum in einen OutputStream speichert. Um die Daten in eine Datei zu holen, ist der OutputStream ein FileOutputStream für eine Datei datum.ser. Der Dateiname wird meist so gewählt, dass er mit .ser endet.

Listing 12.32   SerializeAndDeserializeDate.java, Teil 1

import java.io.*;
import java.util.*;
public class SerializeAndDeserializeDate
{
  static void serialize( String filename )
  {
    try
    {
      FileOutputStream file = new FileOutputStream( filename );
        ObjectOutputStream o = new ObjectOutputStream( file );
          o.writeObject  ( "Today" );
        o.writeObject  ( new Date() );
      o.close();
    }
    catch ( IOException e ) { System.err.println( e ); }
  }
  static void deserialize( String filename )
  {
    // …
  }
  public static void main( String[] args )
  {
    String filename = "c:/datum.ser";
    serialize( filename );
    deserialize( filename );
  }
}

Wollen wir Objekte – oder allgemeiner Daten beziehungsweise Primitive – serialisieren, so benötigen wir einen OutputStream, der die Zustände der Objekte und Metainformationen tatsächlich sichert. In unserem Fall ist das der FileOutputStream. Die Verbindung zwischen der Datei und dem Objektstrom durch die Klasse ObjectOutputStream geschieht über den Konstruktor, der einen OutputStream annimmt. ObjectOutputStream implementiert ObjectOutput, das eine Schnittstelle ist. So besitzt die Klasse ObjectOutput beispielsweise die Funktion writeObject() zum Schreiben von Objekten. Damit wird das Serialisieren des String-Objekts (das »Today«) und des anschließenden Datum-Objekts zum Kinderspiel.


class java.io.  ObjectOutputStream
  extends OutputStream
implements ObjectOutputObjectStreamConstants

gp  ObjectOutputStream( OutputStream out ) throws IOException Erzeugt einen ObjectOutputStream, der in den angegebenen OutputStream schreibt. Ein Fehler kann von den Methoden aus dem OutputStream kommen.

interface java.io.  ObjectOutput
  extends DataOutput

gp  void writeObject( Object obj ) throws IOException Schreibt das Objekt. Die implementierende Klasse weiß, wie das Objekt zu schreiben ist.
gp  void write( int b ) throws IOException Ein Byte wird geschrieben.
gp  void write( byte[] b ) throws IOException Schreibt ein Array von Byte.
gp  void write( byte[] b, int off, int len ) throws IOException Schreibt einen Teil des Arrays. Es werden len Daten des Arrays b ab der Position off geschrieben.
gp  void flush() throws IOException Noch gepufferte Daten werden geschrieben.
gp  void close() throws IOException Der Stream wird geschlossen. Die Methode muss aufgerufen werden, bevor der Datenstrom zur Eingabe verwendet werden soll.

Alle diese Methoden können eine IOException genau dann werfen, wenn Fehler beim Auslesen der Attribute oder beim grundlegenden Schreiben auf dem Datei- beziehungsweise Netzwerksystem auftreten.

Das Interface ObjectOutput erweitert die Schnittstelle DataOutput um das Schreiben von Primitiven: write(byte[]), write(byte[], int, int), write(int), writeBoolean(boolean), writeByte(int), writeBytes(String), writeChar(int), writeChars(String), writeDouble(double), writeFloat(float), writeInt(int), writeLong(long), writeShort(int) und writeUTF(String).

Objekte über das Netzwerk schicken

Es ist natürlich wieder feines OOD, dass es der Methode writeObject() egal ist, wohin das Objekt geschoben wird. Dazu wird ja einfach dem Konstruktor von ObjectOutputStream ein OutputStream übergeben, und writeObject() delegiert dann das Senden der entsprechenden Einträge an die passenden Methoden der Output-Klasse. Im oberen Beispiel benutzten wir ein FileOutputStream. Es sind aber auch noch eine ganze Menge anderer Klassen, die OutputStream erweitern. So können die Objekte auch in einer Datenbank abgelegt beziehungsweise über das Netzwerk verschickt werden. Wie dies funktioniert, zeigen die nächsten Zeilen:

Socket s = new Socket( hostport );
OutputStream os = s.getOutputStream();
ObjectOutputStream oos = new ObjectOutputStream( os );
oos.writeObject( object );

Über s.getOutputStream() gelangen wir an den Datenstrom. Dann sieht alles wie gewohnt aus. Da wir allerdings auf der Empfängerseite noch ein Protokoll ausmachen müssen, werden wir diesen Weg der Objektversendung nicht weiterverfolgen und uns später vielmehr auf eine Technik verlassen, die sich RMI nennt.

Objekte in ein Bytefeld schreiben

Die Klassen ObjectOutputStream und ByteArrayOutputStream sind zusammen zwei gute Partner, wenn es darum geht, eine Repräsentation eines Objekts im Speicher zu erzeugen und die Größe eines Objekts herauszufinden.

Object o = ...;
  ByteArrayOutputStream baos = new ByteArrayOutputStream();
  ObjectOutputStream    oos  =   new ObjectOutputStream( baos );
  oos.writeObject( o );
oos.close();
byte[] array = baos.toByteArray();

Nun steht das Objekt im Bytefeld. Wollten wir die Größe erfragen, müssten wir das Attribut length des Felds auslesen.


Galileo Computing

12.14.2 Objekte über die Standard-Serialisierung lesen  downtop

Zum Lesen der serialisierten Objektzustände dient die Klasse ObjectInputStream. Sie erwartet die Rohdaten wie üblich über einen InputStream. Kommen die Informationen aus einer Datei, verwenden wir wie üblich einen FileInputStream. Die Klasse ObjectInputStream bietet dann über readObject() die Möglichkeit, den String und das Datum auszulesen. Während des Lesens findet readObjekte() den Typ des serialisierten Objekts und baut daraus zur Laufzeit das Zielobjekt auf. Aus den Daten im Datenstrom werden dann die Zustände des Objekts wiederhergestellt. Wenn nötig, restauriert der ObjectInputStream auch Objekte, auf die verwiesen wurde. Die Klasseninformationen müssen zur Laufzeit vorhanden sein, weil bei der Serialisierung nur die Zustände, aber keine .class-Dateien gesichert werden.

Listing 12.33   SerializeAndDeserializeDate.java, Teil 2

  static void deserialize( String filename )
  {
    try
    {
      FileInputStream file = new FileInputStream( filename );
      ObjectInputStream o = new ObjectInputStream( file );
      String string = (String) o.readObject();
      Date date = (Date) o.readObject();
      o.close();
      System.out.println( string );
      System.out.println( date );
    }
    catch ( IOException e ) { System.err.println( e ); }
    catch ( ClassNotFoundException e ) { System.err.println( e ); }
  }

Die explizite Typumwandlung kann natürlich bei einer falschen Zuweisung zu einem Fehler führen.


class java.io.  ObjectInputStream
  extends InputStream
implements ObjectInputObjectStreamConstants

gp  ObjectInputStream( InputStream out ) throws IOException Erzeugt einen ObjectInputStream, der aus einem gegebenen InputStream liest.

Das Interface ObjectInput ist von der gleichen Bauweise wie ObjectOutput. Es erweitert nur DataInput, welches wiederum das Lesen von Primitiven erlaubt.


interface java.io.  ObjectInput
  extends DataInput

gp  Object readObject() throws ClassNotFoundException, IOException Liest ein Object und gibt es zurück. Eine ClassNotFoundException wird ausgelöst, wenn das Objekt zu einer Klasse gehört, die nicht auffindbar ist.
gp  int read() throws IOException Liest ein Byte aus dem Datenstrom. Dieses ist –1, wenn das Ende erreicht ist.
gp  int read( byte[] b ) throws IOException Liest ein Array in den Puffer. Auch hier zeigt –1 das Ende an.
gp  int read( byte[] b, int off, int len ) throws IOException Liest in ein Array von Byte in den Puffer b an der Stelle off genau len Byte.
gp  long skip( long n ) throws IOException Überspringt n Byte im Eingabestrom. Die Anzahl der tatsächlich übersprungenen Zeichen wird zurückgegeben.
gp  int available() throws IOException Gibt die Anzahl der Zeichen zurück, die ohne Blockade gelesen werden können.
gp  void close() throws IOException Schließt den Eingabestrom.

Galileo Computing

12.14.3 Die Schnittstelle Serializable  downtop

Bisher nahmen wir immer an, dass eine Klasse weiß, wie sie geschrieben wird. Das funktioniert wie selbstverständlich bei vielen vordefinierten Klassen, und so müssen wir uns bei writeObject(new Date()) keine Gedanken darüber machen, wie sich das Datum schreibt. Nicht alle Objekte sind jedoch serialisierbar. Zu den nicht serialisierbaren Klassen gehören zum Beispiel Thread, Socket oder viele Klassen aus dem java.io-Paket. Das liegt daran, dass nicht klar ist, wie zum Beispiel ein Wiederaufbau aussehen sollte. Wenn ein Thread geschrieben wird, soll er dann beim Einlesen sofort wieder laufen und dort weitermachen, wo er aufgehört hat?

Damit Objekte serialisiert werden können, müssen die Klassen die Schnittstelle Serializable implementieren. Diese Schnittstelle enthält keine Methoden und ist nur eine Markierungsschnittstelle. Implementiert eine Klasse diese Schnittstelle nicht, folgt beim Serialisierungsversuch eine NotSerializableException. Eine Klasse wie java.util.Date implementiert somit Serializable, Thread jedoch nicht. Der Serialisierer lässt damit alle Klassen »durch«, die instanceof Serializable sind. Daraus folgt, dass alle Unterklassen einer Klasse, die serialisierbar ist, auch ihrerseits serialisierbar sind. So implementiert java.lang.Number – die Basisklasse der Wrapper-Klassen – die Schnittstelle Serializable, und die konkreten Wrapper-Klassen wie Integer, BigDecimal sind somit ebenfalls serialisierbar.

Ob Objekte als Träger sensibler Daten serialisierbar sein sollen, ist gut zu überlegen. Denn bei Serialisierung der Zustände – es werden auch private Attribute serialisiert, an die zunächst nicht so einfach heranzukommen ist – öffnet sich die Kapselung. Aus dem Datenstrom lassen sich die internen Belegungen ablesen und auch manipulieren.


Hinweis   Feld-Objekte sind standardmäßig serialisierbar – sie implementieren versteckt die Schnittstelle Serializable.

Attribute einer Klasse automatisch schreiben

Wir wollen nun eine Klasse TestSer schreib- und lesefähig machen. Dazu benötigen wir das folgende Gerüst:

Listing 12.34   TestSer.java

import java.io.Serializable;
public class TestSer   implements Serializable
  {
  int    a;
  double d;
  static int u;
}

Erzeugen wir ein TestSer-Objekt, nennen wir es ts und rufen writeObject(ts) auf, so schiebt es all seine Variablen (hier a und d) in den Datenstrom.

Statische Variablen werden mit dem Standardserialisierungsmechanismus nicht gesichert. Dies ist auch nicht möglich, weil sich hier verschiedene Objekte ja eine statische Variable miteinander teilen. Wenn zwei Objekte wieder deserialisiert werden, könnte es andernfalls vorkommen, dass beide unterschiedliche Werte aufweisen. Was sollte dann passieren?


Galileo Computing

12.14.4 Nicht serialisierbare Attribute mit transient aussparen  downtop

Es gibt eine Reihe von Objekttypen, die sich nicht serialisieren lassen – technisch gesprochen implementieren diese Klassen die Schnittstelle Serializable nicht. Doch überhaupt Objekte, die nicht persistent gemacht werden sollen? Eine Antwort wäre: Sicherheit! Ein Objekt, das etwa Passwörter speichert, soll nicht einfach geschrieben werden. Da reicht es nicht, dass die Attribute privat sind, denn auch sie werden geschrieben. Der andere Punkt ist die Tatsache, dass sich nicht alle Zustände beim Deserialisieren wiederherstellen lassen. Was ist, wenn ein FileInputStream serialisiert wird? Soll dann bei der Deserialisierung eine Datei geöffnet werden? Was ist, wenn die Datei nicht vorhanden ist? Was ist mit einem Socket oder einem ServerSocket? Da all diese Fragen ungeklärt sind, ist es am einfachsten, diese Klasse nicht die Schnittstelle Serializable implementieren zu lassen.

In diesem Fall haben wir jedoch spätestens dann ein Problem, wenn ein Objekt geschrieben wird, das intern auf ein nicht serialisierbares Objekt – etwa auf einen Thread – verweist.

Abbildung
Hier klicken, um das Bild zu Vergrößern

Die Serialisierung der folgenden Klasse bringt einen Laufzeitfehler ein

Listing 12.35   SerializeTransient.java

import java.io.*;
public class SerializeTransient
{
  public static void main( String[] args ) throws Exception
  {
    ByteArrayOutputStream bytearray = new ByteArrayOutputStream();
    ObjectOutputStream oos = new ObjectOutputStream( bytearray );
    @SuppressWarnings( "all" )
    class NotTransientNotSerializable implements Serializable
    {
        Thread t = new Thread();
  //      transient Thread t = new Thread();
      String s = "Fremde sind Freunde, die man nur noch nicht kennengelernt hat.";
    }
    oos.writeObject( new NotTransientNotSerializable() );
    oos.close();
    System.out.println( bytearray.toString() );
  }
}

Der Fehler wird eine NotSerializableException sein:

java.io.NotSerializableException: java.lang.Thread
 at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1054)
 at java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.java:1330)
 at java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1302)
 at java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1245)
 at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1052)
 at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:278)
 at SerializeTransient.main(SerializeTransient.java:18)
Exception in thread "main"

Die Begründung dafür ist einfach: Ein Thread lässt sich nicht serialisieren. Falls wir aber ein Objekt vom Typ NotTransientNotSerializable ohne Thread serialisieren wollen, müssen wir dem Serialisierungsmechanismus mitteilen: »Nimm so weit alle Objekte, aber nicht den Thread!« Dazu gibt es in Java ein spezielles Schlüsselwort: transient, das alle Attribute markiert, die nicht persistent sein sollen. Damit lassen wir die nicht serialisierbaren Kandidaten außen vor und speichern alles ab, was sich speichern lässt.

Um das Beispiel zu Ende zu bringen, setzen wir den Modifizierer transient vor den Variablentyp – das Beispiel ist ablauffähig.

transient Thread t = new Thread();

Galileo Computing

12.14.5 Das Abspeichern selbst in die Hand nehmen  downtop

Es kann nun passieren, dass es beim Serialisieren nicht genügt, die normalen Attribute zu sichern. Für diesen Fall müssen spezielle Methoden implementiert werden. Beide müssen die nachstehenden Signaturen aufweisen:

private synchronized void writeObject( java.io.ObjectOutputStream s )
  throws IOException

und

private synchronized void readObject( java.io.ObjectInputStream s )
  throws IOExceptionClassNotFoundException

Die Methode writeObject() ist für das Schreiben verantwortlich. Ist der Rumpf leer, gelangen keine Informationen in den Strom, und das Objekt wird folglich nicht gesichert.

Mit diesen Funktionen können wir also die Serialisierung selbst in die Hand nehmen und die Attribute so speichern, wie wir es für sinnvoll halten; eine Kompatibilität lässt sich erzwingen. Eine kleine Versionsnummer im Datenstrom könnte eine Verzweigung provozieren, in der die Daten der Version 1 oder andere Daten der Version 2 gelesen werden. Auch können auf diese Weise statische Attribute in den Datenstrom gelangen.

Beim Lesen können komplette Objekte wieder aufgebaut werden, und es lassen sich zum Beispiel nicht transiente Objekte wieder beleben. Stellen wir uns einen Thread vor, dessen Zustände beim Schreiben persistent gemacht werden, und beim Lesen wird ein Thread-Objekt wieder erzeugt und zum Leben erweckt.

Oberklassen serialisieren sich gleich mit

Wird eine Klasse serialisiert, so wird automatisch die Informationen der Oberklasse mit serialisiert. Hierbei gilt, dass wie beim Konstruktur erst die Attribute der Oberklasse in den Datenstrom geschrieben werden und anschließend die Attribute der Unterklasse. Insbesondere bedeutet dies, dass die Unterklasse nicht noch einmal die Attribute der Oberklasse speichern sollte. Das folgende Programm zeigt den Effekt:

Listing 12.36   WriteTop.java

import java.io.*;
class Base implements Serializable
{
  private void writeObject( ObjectOutputStream oos )
  {
    System.err.println( "Base" );
  }
}
public class WriteTop extends Base implements Serializable
{
  public static void main( String[] args ) throws IOException
  {
    ObjectOutputStream oos = new ObjectOutputStream( System.out );
    oos.writeObject( new WriteTop() );
  }
  private void writeObject( ObjectOutputStream oos )
  {
    System.err.println( "Top" );
  }
}

In der Ausgabe von Eclipse ist die Ausgabe »Base« und »Top« andersfarbig zu erkennen.

Abbildung
Hier klicken, um das Bild zu Vergrößern

Doch noch den Standardserialisierer nutzen

Die Funktionen read/writeObject() sind Alles-oder-nichts-Funktionen. Erkennt der Serialisierer, dass die Schnittstelle Serializable implementiert wird, fragt er die Klasse, ob sie die Methoden implementiert. Wenn nicht, beginnt bei der Serialisierung der Serialisierungsmechanismus eigenständig die Attribute auszulesen und in den Datenstrom zu schreiben. Gibt es die read/writeObject()-Methoden, so wird der Serialisierer diese aufrufen und nicht selbst die Objekte nach den Werten fragen oder die Objekte mit Werten füllen.

Doch die Arbeit des Serialisierers ist eine große Hilfe. Falls viele Attribute zu speichern sind, fällt viel lästige Arbeit beim Programmieren an, da für jedes zu speichernde Attribut eine eigene write-Funktion und beim Lesen eine entsprechende read-Funktion benötigt werden. Aus diesem Dilemma gibt es einen Ausweg, weil der Serialisierer in den read/writeObject()-Methoden auch nachträglich dazu verpflichtet werden kann, die nicht transienten Attribute zu lesen oder zu schreiben. Die privaten Funktionen readObject() und writeObject() bekommen als Parameter ein ObjectInputStream und ein ObjectOutputStream, die über die entsprechende Funktion verfügen.


Hinweis   Es ist gar nicht so abwegig, nur eine readObject(), aber keine writeObject()-Funktion zu implementieren. In readObject() lässt ein defaultReadObject() alle Eigenschaften initialisieren und danach noch Initialisierungsarbeit ähnlich einem Konstruktor durchführen. Dazu zählt etwa Initialisierung von transienten Attributen, Registrierung von Listenern und Weiteres.

Die Klasse ObjectOutputStream erweitert java.io.OutputStream unter anderem um die Methode defaultWriteObject(). Sie speichert die Attribute einer Klasse.


class java.io.  ObjectOutputStream
  extends OutputStream
implements ObjectOutputObjectStreamConstants

gp  public final void defaultWriteObject() throws IOException Schreibt alle nicht statischen und nicht transienten Attribute in den Datenstrom. Die Methode kann nur innerhalb einer privaten writeObject()-Funktion aufgerufen werden; andernfalls erhalten wir eine NotActiveException.

Das Gleiche gilt für die Funktion defaultReadObject() in der Klasse ObjectInputStream.


Beispiel   Eine Klasse definiert zwei Attribute: freundin und alter. Da Frauen über ihr Alter nicht sprechen, soll alter nicht serialisert werden; es ist transient. Wir implementieren eigene read/writeObject()-Funktionen, die den Standardserialisierer bemühen, sonst aber nichts Interessantes bewirken.

Listing 12.37   DefaultReadWriteObject.java

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
public class DefaultReadWriteObject implements Serializable
{
  public String freundin = "Tatjana";
  public transient int alter = 30;
  private void writeObject( ObjectOutputStream oos ) throws IOException
  {
    oos.defaultWriteObject();  // Schreibe freundin, aber nicht alter
  }
  private void readObject( ObjectInputStream ois ) throws IOException
  {
    try
    {
      ois.defaultReadObject();  // Lese freundin, aber nicht das alter
    }
    catch ( ClassNotFoundException e )
    {
      throw new IOException( "Klasse nicht gefunden. HILFE!!" );
    }
  }
}


Galileo Computing

12.14.6 Tiefe Objektkopien  downtop

Klassen können die clone()-Methode von Object überschreiben und so eine Kopie der Werte liefern. Die Standardimplementierung ist jedoch so angelegt, dass diese Kopie flach ausfällt. Das bedeutet: Referenzen auf Objekte, die von dem zu klonenden Objekt ausgehen, werden beibehalten und diese Objekte nicht extra kopiert. Als Beispiel kann die einfache Datenstruktur eines Felds genügen, das auf Vector-Objekte verweist. Ein Klon dieses Felds ist lediglich ein zweites Feld, dessen Elemente auf die gleichen Vektoren zeigen. Eine Änderung wird also beiden Feldern bewusst.

Möchten wir das Verhalten ändern und eine tiefe Kopie anfertigen, so haben wir dank eines kleinen Tricks damit keine Mühe: Wir könnten das zu klonende Objekt einfach serialisieren und dann wieder auspacken. Die zu klonenden Objekte müssen dann nur das Serializable-Interface implementieren.

Listing 12.38   Dolly.java, deepCopy()

public static Object deepCopy( Object o ) throws Exception
{
  ByteArrayOutputStream baos = new ByteArrayOutputStream();
  new ObjectOutputStream( baos ).writeObject( o );
  ByteArrayInputStream bais = new ByteArrayInputStream( baos.toByteArray() );
  return  new ObjectInputStream(bais).readObject();
}

Das Einzige, was wir zum Gelingen der Methode deepCopy() beitragen müssen, ist, das Objekt in einem Bytefeld zu serialisieren, es wieder auszulesen und zu einem Objekt zu konvertieren. Den Einsatz eines ByteArrayOutputStream haben wir schon beobachtet, als wir die Länge eines Objekts herausfinden wollten. Nun fügen wir das Feld einfach wieder zu einem ByteArrayInputStream hinzu, aus dessen Daten dann ObjectInputStream das Objekt rekreieren kann.

Überzeugen wir uns anhand eines kleinen Programms, dass die tiefe Kopie tatsächlich etwas anderes als ein clone() ist.

Listing 12.39   Dolly.java, main()

public static void main( String[] args ) throws Exception
{
  Map<String,String> map = new HashMap<String,String>();
  map.put( "Cul de Paris",
           "hinten unter dem Kleid getragenes Gestell oder Polster" );
  LinkedList<Map> l1 = new LinkedList<Map>();
  l1.add( map );
  List l2 = (List) l1.clone();
  List l3 = (List) deepCopy( l1 );
  map.clear();
  System.out.println( l1 ); // [{}]
  System.out.println( l2 ); // [{}]
  System.out.println( l3 ); // [{Cul de Paris=hinten unter dem Kleid ...}]
}

Zunächst erstellen wir eine Map, die wir anschließend in eine Liste packen. Die Map enthält ein Pärchen. Klonen wir mit clone() die Liste, so wird zwar die Liste selbst kopiert, aber nicht die Map. Die tiefe Kopie kopiert neben der Liste auch gleich die Map mit. Das sehen wir dann, wenn wir den Eintrag aus der Map löschen. Dann ergibt l1 genauso wie l2 eine leere Liste, da l2 nur die Verweise auf die Map gespeichert hat, die dann aber geleert ist. Anders ist dies bei l3, der tiefen Kopie; hier ist das Paar noch vorhanden. An diesem Beispiel sehen wir, wie wunderbar die Stream-Klassen zusammenarbeiten. Einzige Voraussetzung zum Gelingen ist die Implementierung der Schnittstelle Serializable. Da die zu klonenden Klassen aber auch clone() implementieren müssen, gilt in der Regel, dass sie serialisierbar sind. Daher stehen in der implements-Zeile die Schnittstellen Clonable und Serializable direkt nebeneinander.


Galileo Computing

12.14.7 Versionenverwaltung und die SUID  downtop

Die erste Version einer Klassenbibliothek ist in der Regel nicht vollständig und nicht beendet. Es kann gut sein, dass Attribute und Methoden nachträglich in die Klasse eingefügt, gelöscht oder modifiziert werden. Das bedeutet aber auch, dass die Serialisierung zu einem Problem werden kann. Denn ändert sich der Typ einer Variablen oder kommen Variablen hinzu, dann ist eine gespeicherte Objektserialisierung nicht mehr gültig.

Bei der Serialisierung wird in Java nicht nur der Objektinhalt geschrieben, sondern zusätzlich eine eindeutige Kennung der Klasse, die UID. Die UID ist ein Hashcode aus Namen, Attributen, Parametern, Sichtbarkeit und so weiter. Sie wird als long wie ein Attribut gespeichert. Ändert sich der Aufbau einer Klasse, ändert sich der Hashcode und damit die UID. Klassen mit unterschiedlicher UID sind nicht kompatibel. Erkennt der Lesemechanismus in einem Datenstrom eine UID, die nicht zur Klasse passt, wird eine InvalidClassException ausgelöst. Das bedeutet, dass schon ein einfaches Hinzufügen von Attributen zu einem Fehler führt.

Wir wollen uns dies einmal anhand einer einfachen Klasse ansehen. Wir entwickeln eine Klasse SerMe mit einem einfachen Ganzzahlattribut. Später fügen wir eine Fließkommazahl hinzu.

Listing 12.40   InvalidSer.java, Teil 1

class SerMe implements Serializable
{
  int i;
//  double d;
//  float i;
}

Dann benötigen wir noch das Hauptprogramm. Wir bilden ein Exemplar von SerMe und schreiben es in eine Datei. Ohne Änderungen können wir es direkt wieder deserialisieren. Ändern wir jedoch die Klassendefinition, führt dies zu einem Fehler.

Listing 12.41   InvalidSer.java, Teil 2

import java.io.*;
public class InvalidSer
{
  public static void main( String[] args ) throws Exception
  {
    String filename = "c:/test.ser";
    // Teil 1: Schreiben
    //    ObjectOutputStream oo = new ObjectOutputStream(
    //      new FileOutputStream( filename ) );
    //    oo.writeObject( new SerMe() );
    //    oo.close();
    // Teil 2: Klasse SerMe ännder und Lesen versuchen
    ObjectInputStream oi = new ObjectInputStream(
      new FileInputStream( filename ) );
    SerMe o = (SerMe) oi.readObject();
    oi.close();
  }
}

Fügen wir der Klasse SerMe das Attribut double d zu oder ändern wir den Typ der Ganzzahlvariablen auf float, folgt eine lange Fehlerliste:

java.io.InvalidClassException: SerMe; Local class not compatible:
stream classdesc serialVersionUID=9027745268614067035
local class serialVersionUID=-3271853622578609637
    at java.io.ObjectStreamClass.validateLocalClass(ObjectStreamClass.java:523)
    at java.io.ObjectStreamClass.setClass(ObjectStreamClass.java:567)
    at ujava.io.ObjectInputStream.inputClassDescriptor(ObjectInputStream.java:936)
    at java.io.ObjectInputStream.readObject(ObjectInputStream.java:366)
    at java.io.ObjectInputStream.readObject(ObjectInputStream.java:236)
    at java.io.ObjectInputStream.inputObject(ObjectInputStream.java:1186)
    at java.io.ObjectInputStream.readObject(ObjectInputStream.java:386)
    at java.io.ObjectInputStream.readObject(ObjectInputStream.java:236)
    at InvalidSer.main(InvalidSer.java:28)

Die eigene SUID

Dem oberen Fehlerauszug entnehmen wir, dass der Serialisierungsmechanismus die SUID selbst berechnet. Das Attribut ist als statische, finale Variable mit dem Namen serialVersionUID in der Klasse abgelegt. Ändern sich die Klassenattribute, ist es günstig, eine eigene SUID einzutragen, denn der Mechanismus zum Deserialisieren kann dann etwas gutmütiger mit den Daten umgehen. Beim Einlesen gibt es nämlich Informationen, die nicht hinderlich sind. Wir sprechen in diesem Zusammenhang auch von Stream-kompatibel. Dazu gehören zwei Bereiche.

Neue Felder

Befinden sich in der neuen Klasse Attribute, die im Datenstrom nicht benannt sind, werden diese Attribute mit 0 oder null initialisiert.

Fehlende Felder

Befinden sich im Datenstrom Attribute, die in der neuen Klasse nicht vorkommen, werden sie einfach ignoriert.

Die SUID lässt sich mit einem kleinen Dienstprogramm serialver berechnen. Auf diese Weise erreichen wir eine Stream-kompatible Serialisierung.


Beispiel   Dies wollen wir für unsere Klasse SerMe mit dem Dienstprogramm testen:
 serialver SerMe
  SerMe:    static final long serialVersionUID = 9027745268614067035L;

Diese letzte Zeile können wir in unsere Klasse SerMe kopieren. Nehmen wir jetzt noch eine Fließkommazahl d hinzu, dann wird die InvalidClassException nicht mehr auftreten, da mit der Hinzunahme eines Attributs die Stream-Kompatibilität gewährleistet ist.

class SerMe implements Serializable
{
  static final long serialVersionUID = 9027745268614067035L;
  int i;
  double d;
}

Galileo Computing

12.14.8 Wie die ArrayList serialisiert  downtop

Am Beispiel einer java.util.ArrayList lässt sich sehr schön beobachten, wie sich die Funktionen writeObject() und readObject() nutzen lassen. Eine ArrayList beinhaltet eine Reihe von Elementen. Zur Speicherung nutzt die Datenstruktur ein internes Feld. Das Feld kann größer als die Anzahl der Elemente sein, damit bei jedem add() das Feld nicht immer neu vergrößert werden muss. Nehmen wir an, die ArrayList würde eine Standardserialisierung nutzen. Was passiert nun? Es könnte das Problem entstehen, dass bei nur einem Objektverweis in der Liste und einer internen Feldgröße von 1000 Elementen leider 999 null-Verweise gespeichert würden. Das wäre aber Verschwendung! Besser ist es, eine angepasste Serialisierung zu verwenden.

Wir schauen uns einmal den Quellcode der Methoden aus dem Projekt GNU Classpath an:

private void writeObject(ObjectOutputStream s) throws IOException
{
    // The 'size' field.
  s.defaultWriteObject();
  // We serialize unused list entries to preserve capacity.
  int len = data.length;
  s.writeInt(len);
  // it would be more efficient to just write 'size' items,
  // this need readObject read 'size' items too.
  for (int i = 0; i < size; i++)
    s.writeObject(data[i]);
}
private void readObject(ObjectInputStream s)
  throws IOExceptionClassNotFoundException
{
  // the 'size' field.
  s.defaultReadObject();
  int capacity = s.readInt();
  data = new Object[capacity];
  for (int i = 0; i < size; i++)
    data[i] = s.readObject();
}

Galileo Computing

12.14.9 Probleme mit der Serialisierung  downtop

Der klassische Weg von einem Objekt zu einer persistenten Speicherung führt über den Serialisierungsmechanismus von Java über die Klassen ObjectOutputStream und ObjectInputStream. Die Serialisierung in Binärdaten ist aber nicht ohne Nachteile. Schwierig ist beispielsweise die Weiterverarbeitung von Nicht-Java-Programmen oder die nachträgliche Änderung ohne Einlesen und Wiederaufbauen der Objektverbunde. Wünschenswert ist daher eine Textrepräsentation. Diese hat nicht die oben genannten Nachteile.

Ein weiteres Problem ist die Skalierbarkeit. Die Standard-Serialisierung arbeitet nach dem Prinzip: Alles, was von einem Basisknoten aus erreichbar ist, wird in den Datenstrom geschrieben. Ist der Objektgraf sehr groß, steigt die Zeit für die Serialisierung und das Datenvolumen an. Verglichen mit anderen Persistenz-Konzepten, ist es nicht möglich, nur die Änderungen zu schreiben. Wenn sich zum Beispiel in einer sehr großen Adressliste die Hausnummer einer Person ändert, muss die gesamte Adressliste neu geschrieben werden – das nagt an der Performanz.

Auch parallele Änderungen können zum Problem werden, da die Serialisierung über kein transaktionales Konzept verfügt. Während der Serialisierung sind die Objekte und Datenstrukturen nicht gesperrt, und ein anderer Thread kann derweil alles Mögliche modifizieren. Der Entwickler muss sich selbst auferlegen, während des Schreibens keine Änderungen vorzunehmen, damit der Schreibzugriff isoliert ist.


Galileo Computing

12.14.10 Serialisieren in XML-Dateien  downtop

Soll ein Objektgraf in XML serialisiert werden, finden wir mittlerweile viele Bibliotheken, die die Weiterverarbeitung sichern.

gp  XStream (http://xstream.codehaus.org/)
gp  Commons Betwixt (http://jakarta.apache.org/commons/betwixt/)
gp  Castor (http://www.castor.org/)
gp  Zeus (http://zeus.objectweb.org/, http://forge.objectweb.org/projects/zeus/)
gp  Java Architecture for XML Binding: JAXB (http://java.sun.com/xml/jaxb/)

Galileo Computing

12.14.11 JavaBeans Persistence  downtop

Um in XML zu schreiben und von dort zu laden, werden die Klassen ObjectOutputStream und ObjectInputStream durch die Klassen XMLEncoder und XMLDecoder ersetzt.

Abbildung
Hier klicken, um das Bild zu Vergrößern

Die folgende Klasse ist unserem Programm SerializeAndDeserialize nachempfunden. Ersetzen müssen wir lediglich die Object-Streams. Die Klassen XMLEncoder und XMLDecoder liegen auch nicht in java.io, sondern unter dem Paket java.beans. Interessanterweise muss die Ausnahme ClassNotFoundException nicht mehr aufgefangen werden.

Listing 12.42   SerializeAndDeserializeXML.java

import java.io.*;
import java.util.Date;
import java.beans.*;
public class SerializeAndDeserializeXML
{
  public static void main( String[] args )
  {
    String filename = "datum.ser.xml";
    // Serialize
    XMLEncoder enc = null;
    try
    {
      enc = new XMLEncoder( new FileOutputStream(filename) );
      enc.writeObject( "Today" );
      enc.writeObject( new Date() );
    }
    catch ( IOException e ) {
      e.printStackTrace();
    }
    finally {
      if ( enc != null )
        enc.close();
    }
    // Deserialize()
    XMLDecoder dec = null;
    try
    {
      dec = new XMLDecoder( new FileInputStream(filename) );
      String string = (String) dec.readObject();
      Date   date   = (Date)   dec.readObject();
      System.out.println( string );
      System.out.println( date );
    }
    catch ( IOException e ) {
      e.printStackTrace();
    }
    finally {
      if ( enc != null )
        dec.close();
    }
  }
}

Und so sehen wir nach dem Ablauf des Programms in der Datei datum.ser.xml Folgendes:

<?xml version="1.0" encoding="UTF-8"?>
<java version="1.5.0_03" class="java.beans.XMLDecoder">
 <string>Today</string>
 <object class="java.util.Date">
  <long>1119355982375</long>
 </object>
</java>

Bei eigenen Objekten gilt es immer zu bedenken, dass die XML-Serialisierer von Sun nur Beans schreibt. Eigene Klassen müssen daher immer ihre serialisierbaren Eigenschaften über getXXX()/setXXX()-Methoden bereitstellen, müssen aber dafür die Markierungsschnittstelle Serializable nicht implementieren.


Galileo Computing

12.14.12 XStream  toptop

XStream ist eine quelloffene Software unter der BSD-Lizenz, mit der sich serialisierbare Objekte in XML umwandeln lassen. Damit ähnelt XStream eher der Standard-Serialisierung als der JavaBeans Persistence. Nachdem die unter http://xstream.codehaus.org/download.html heruntergeladene Bibliothek xstream-x.y.jar sowie der schnelle XML-Parser xpp3-x.y.jar auf der gleichen Seite eingebunden sind, ist ein Beispielprogramm schnell formuliert:

Point p = new Point( 12032 );
XStream xstream = new XStream();
String xml = xstream.toXML( p );
System.out.println( xml );
Point q = (Point) xstream.fromXML( xml );

Ausnahmen müssen angenehmerweise nicht aufgefangen werden. Der String xml enthält:

<java.awt.Point>
<x>120</x>
<y>32</y>
</java.awt.Point>



1  Die Rede ist hier von RMI.

 << zurück




Copyright © Galileo Press GmbH 2005
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. 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.


[Galileo Computing]

Galileo Press GmbH, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, info@galileo-press.de