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


Java ist auch eine Insel von Christian Ullenboom
Programmieren für die Java 2-Plattform in der Version 1.4
Buch: Java ist auch eine Insel - Zum Katalog
gp Kapitel 12 Datenströme und Dateien
  gp 12.1 Dateien und Verzeichnisse
    gp 12.1.1 Dateien und Verzeichnisse mit der Klasse File
    gp 12.1.2 Dateieigenschaften und -attribute
    gp 12.1.3 Umbenennen, Verzeichnisse anlegen und Datei löschen
    gp 12.1.4 Die Wurzel aller Verzeichnisse
    gp 12.1.5 Verzeichnisse listen und Dateien filtern
    gp 12.1.6 Implementierungsmöglichkeiten für die Klasse File
    gp 12.1.7 Verzeichnisse nach Dateien rekursiv durchsuchen
  gp 12.2 Dateien mit wahlfreiem Zugriff
    gp 12.2.1 Eine RandomAccessFile öffnen
    gp 12.2.2 Aus dem RandomAccessFile lesen
    gp 12.2.3 Hin und her in der Datei
    gp 12.2.4 Die Länge des RandomAccessFile
  gp 12.3 Übersicht über wichtige Stream- und Writer/Reader
    gp 12.3.1 Die abstrakten Basisklassen
  gp 12.4 Eingabe- und Ausgabe-Klassen: InputStream und OutputStream
    gp 12.4.1 Die Klasse OutputStream
    gp 12.4.2 Ein Datenschlucker
    gp 12.4.3 Die Eingabeklasse InputStream
    gp 12.4.4 Anwenden der Klasse FileInputStream
    gp 12.4.5 Anwendung der Klasse FileOutputStream
    gp 12.4.6 Kopieren von Dateien
    gp 12.4.7 Daten filtern durch FilterInputStream und FilterOutputStream
    gp 12.4.8 Der besondere Filter PrintStream
    gp 12.4.9 System Standard-Ein- und Ausgabe und Input- bzw. PrintStreams
    gp 12.4.10 Bytes in den Strom mit ByteArrayOutputStream
    gp 12.4.11 Ströme zusammensetzen mit SequenceInputStream
  gp 12.5 Die Unterklassen von Writer
    gp 12.5.1 Die abstrakte Klasse Writer
    gp 12.5.2 Datenkonvertierung durch den OutputStreamWriter
    gp 12.5.3 In Dateien schreiben mit der Klasse FileWriter
    gp 12.5.4 StringWriter und CharArrayWriter
    gp 12.5.5 Gepufferte Ausgabe durch BufferedWriter
    gp 12.5.6 Ausgabemöglichkeiten durch PrintWriter erweitern
    gp 12.5.7 Daten mit FilterWriter filtern
    gp 12.5.8 Die abstrakte Basisklasse Reader
    gp 12.5.9 Automatische Konvertierungen mit dem InputStreamReader
    gp 12.5.10 Dateien lesen mit der Klasse FileReader
    gp 12.5.11 StringReader und CharArrayReader
    gp 12.5.12 Schachteln von Eingabe-Streams
    gp 12.5.13 Gepufferte Eingaben mit der Klasse BufferedReader
    gp 12.5.14 LineNumberReader zählt automatisch Zeilen mit
    gp 12.5.15 Eingaben filtern mit der Klasse FilterReader
    gp 12.5.16 Daten zurücklegen mit der Klasse PushbackReader
  gp 12.6 Kommunikation zwischen Threads mit Pipes
    gp 12.6.1 PipedOutputStream und PipedInputStream
    gp 12.6.2 PipedWriter und PipedReader
    gp 12.6.3 Datenströme komprimieren
    gp 12.6.4 Zip-Archive
  gp 12.7 Prüfsummen
    gp 12.7.1 Die Schnittstelle Checksum
    gp 12.7.2 Die Klasse CRC32
    gp 12.7.3 Die Adler32-Klasse
  gp 12.8 Persistente Objekte und Serialisierung
    gp 12.8.1 Objekte speichern
    gp 12.8.2 Objekte lesen
    gp 12.8.3 Die Schnittstelle Serializable
    gp 12.8.4 Ian Wilmut10 und tiefe Objektkopien
    gp 12.8.5 Felder sind implizit Serializable
    gp 12.8.6 Versionenverwaltung und die SUID
    gp 12.8.7 Beispiele aus den Standard-Klassen
    gp 12.8.8 Serialisieren in XML-Dateien
    gp 12.8.9 JSX (Java Serialization to XML)
    gp 12.8.10 XML-API von Sun
  gp 12.9 Die Logging-API

Kapitel 12 Datenströme und Dateien

Schlagfertigkeit ist jede Antwort, die so klug ist,
dass der Zuhörer wünscht, er hätte sie gegeben.
– Elbert Hubbard

Computer sind uns deswegen so nützlich, da sie Daten bearbeiten. Dieser Bearbeitungszyklus beginnt beim Einlesen der Daten, beinhaltet das Verarbeiten und lässt die Ausgabe folgen. In der deutschen Literatur taucht dies als EVA -Prinzip der Datenverarbeitungsanlagen auf. In frühen EDV-Zeiten wurde die Eingabe vom Systemoperator auf Lochkarten gestanzt, doch glücklicherweise sind diese Zeiten vorbei. Heutzutage speichern wir unsere Daten in Dateien (engl. Files ) ab. Da auch ein Programm aus Daten besteht, ist dies nur eine bestimmte Form einer Datei. Wichtig zu bemerken ist, dass eine Datei nur durch den Kontext interessant ist, andernfalls beinhaltet sie für uns keine Information – die Sichtweise auf eine Datei ist demnach wichtig.

Um an die Information einer Datei zu kommen, müssen wir den Inhalt auslesen können. Auch müssen wir in der Lage sein, Dateien anzulegen, zu löschen, umzubenennen und sie in Verzeichnissen zu strukturieren. Java bietet uns eine Vielzahl von Zugriffsmöglichkeiten auf Dateien und ein wichtiges Schlagwort ist hierbei der Datenstrom (engl. Stream). Dieser entsteht beim Fluss der Daten von der Eingabe zur Verarbeitung hin zur Ausgabe. Durch Datenströme können Daten sehr elegant bewegt werden, ein Programm ohne Datenfluss ist eigentlich nicht denkbar. Die Eingabeströme (engl. Input-Streams) sind zum Beispiel Tastatur, Datei oder Netzwerk und über die Ausgabeströme (engl. Output-Streams) fließen die Daten in ein Ausgabemedium, beispielsweise Drucker oder Datei, hinein. Die Kommunikation der Threads geschieht über Pipes. Sie sind eine spezielle Variante der Datenströme.

In Java sind über dreißig Klassen zur Verarbeitung der Datenströme vorgesehen. Da die Datenströme allgemein und nicht an ein spezielles Ein- oder Ausgabeobjekt gebunden sind, können sie untereinander beliebig gemischt werden. Dies ist vergleichbar mit dem elektrischen Strom. Es gibt mehrere Stromlieferanten (Solarturmkraftwerke, Nutzung geothermischer Energie, Umwandlung von Meereswärmeenergie (OTEC)) und mehrere Verbraucher (Wärmedecke, Mikrowelle, Auto), die die Energie wieder umsetzen.


Galileo Computing

12.1 Dateien und Verzeichnisse  downtop

Bisher haben wir uns nur um Datenströme gekümmert. Doch auch die Verwaltung von Dateien ist wichtig: Wie wollen wir durch Datenströme Dateien löschen oder umbenennen? Brauchen wir Informationen über eine Datei, so ist der Angelpunkt ein File-Objekt. Dieses Objekt wurde eingeführt, um Dateioperationen plattformunabhängig durchzuführen. Dies bedeutet aber leider auch eine Einschränkung, denn der wie sollten Rechte vergeben werden, wenn etwa der Macintosh oder ein Palm-Pilot das nicht unterstützt? Auch UNIX und Windows haben zwei völlig verschiedene Ansätze.


Galileo Computing

12.1.1 Dateien und Verzeichnisse mit der Klasse File  downtop

Ein konkretes File-Objekt repräsentiert eine Datei oder ein Verzeichnis auf dem Dateisystem. Der Verweis wird durch einen Pfadnamen spezifiziert. Dieser kann absolut oder relativ zum aktuellen Verzeichnis angegeben werden.

Hinweis Pfadangaben sind plattformabhängig: Die Angabe des Pfads ist plattformabhängig, dass heißt, auf Windows-Rechnern trennt ein Backslash die Pfade (»temp\doof«) und auf UNIX-Maschinen ein normaler Divis (»temp/doof«). Glücklicherweise können wir die Klasse File nach dem separatorChar fragen. Ebenso wie bei den Pfadtrennern gibt es einen Unterschied in der Darstellung des Wurzelverzeichnisses. Unter UNIX ist dies ein einzelnes Divis/«), und unter Windows ist die Angabe des Laufwerks vor dem Doppelpunkt und dem Backslash-Zeichen gestellt (»Z:\«).

class java.io.File
implements Serializable, Comparable

Wir können ein File-Objekt durch drei Konstruktoren erzeugen:

gp  File( String path )
Erzeugt ein File-Objekt mit kompletten Pfadnamen, zum Beispiel »d:\dali\die_anpassung_der_begierde «
gp  File( String path, String name )
Pfadname und Dateiname sind getrennt.
gp  File( File dir, String name )
Der Pfad ist mit einem anderen File-Objekt verbunden.

URL-Objekte aus einem File-Objekt

Da es bei URL-Objekten recht häufig vorkommt, dass eine Datei die Basis ist, wurde die Methode toURL() der Klasse File aufgenommen. Es muss nur ein File-Objekt erzeugt werden, und anschließend erzeugt toURL() ein URL-Objekt, welches das Protokoll »file« trägt und eine absolute Pfadangabe zur Datei bzw. zum Verzeichnis enthält. Da diese Methode das Trennzeichen für die Pfade beachtet, ist die Angabe demnach auch passend für die Plattform. Ein Verzeichnis endet passend mit dem Pfadtrenner.

class java.io.File
implements Serializable, Comparable

gp  URL toURL() throws MalformedURLException
Liefert ein URL-Objekt vom File-Objekt.

Galileo Computing

12.1.2 Dateieigenschaften und -attribute  downtop

Eine Datei oder ein Verzeichnis besitzt zahlreiche Methoden, um die Eigenschaften wie Dateilänge, Attribute auszulesen.

gp  boolean canRead()
true
, wenn wir lesend zugreifen dürfen.
gp  boolean canWrite()
true
, wenn wir schreibend zugreifen dürfen.
gp  boolean exists()
true
, wenn das File-Objekt existiert.
gp  String getAbsolutePath()
Liefert den absoluten Pfad. Ist das Objekt kein absoluter Pfadname, so wird ein String aus aktuellem Verzeichnis, Separatorzeichen und Dateinamen des Objekts verknüpft.
gp  String getCanonicalPath()
Gibt den Pfadnamen des Dateiobjekts zurück, der keine relativen Pfadangaben mehr enthält. Kann im Gegensatz zu den anderen Pfad-Funktionen eine IOException aufrufen, da mitunter verbotene Dateizugriffe erfolgen.
gp  String getName()
Gibt Dateinamen zurück.
gp  String getParent()
Gibt Pfadnamen des Vorgängers zurück.
gp  String getPath()
Gibt Pfadnamen zurück.
gp  boolean isAbsolute()
true
, wenn der Pfad in der systemabhängigen Notation absolut ist.
gp  boolean isDirectory()
Gibt true zurück, wenn es sich um ein Verzeichnis handelt.
gp  boolean isFile()
true
, wenn es sich um eine »normale« Datei handelt (kein Verzeichnis und keine Datei, die vom zu Grunde liegenden Betriebssystem als besonders markiert wird; Blockdateien, Links unter UNIX). In Java können nur normale Dateien erzeugt werden.
gp  long length()
Gibt die Länge der Datei in Bytes zurück oder 0L, wenn die Datei nicht existiert.
Beispiel Um festzustellen, ob wir uns im Wurzelverzeichnis befinden, genügt folgende Zeile:
if ( f.getPath().equals(f.getParent()) )
 // File f ist root

Änderungsdatum einer Datei

Eine Datei besitzt unter jedem Dateisystem nicht nur Attribute wie Größe und Rechte, sondern verwaltet auch das Datum der letzten Änderung. Letzteres nennt sich Zeitstempel. Die File-Klasse besitzt zum Abfragen dieser Zeit die Methode lastModified(). Mit dem JDK 1.2 ist die Methode setLastModified() hinzugekommen, um auch diese Zeit zu setzen. Dabei bleibt es etwas verwunderlich, warum lastModified() nicht veraltet ausgezeichnet ist und zu getLastModified() geworden ist, wo doch nun die passende Funktion zum Setzen der Namensgebung genügt.

class java.io.File
implements Serializable, Comparable

gp  long lastModified()
Liefert die Zeit, zu der die Datei zum letzten Mal geändert wurde. Die Zeit wird in Millisekunden ab dem 1. Januar 1970, 00:00:00 GMT gemessen. Die Methode liefert null, wenn die Datei nicht existiert oder ein Ein- bzw. Ausgabefehler auftritt.
gp  boolean setLastModified( long time )
Setzt die Zeit, an dem die Datei zuletzt geändert wurde. Die Zeit ist wiederum in Millisekunden seit dem 1. 1. 1970 angegeben. Ist das Argument negativ, dann wird eine IllegalArgumentException geworfen oder eine SecurityException, wenn ein SecurityManager existiert und dieser das Ändern verbietet.

Die Methode setLastModified() ändert wenn möglich den Zeitstempel, und ein anschließender Aufruf von lastModified() liefert die gesetzte Zeit – womöglich gerundet – zurück. Die Funktion ist von vielfachem Nutzen, ist aber sicherheitsbedenklich. Denn ein Programm kann den Dateiinhalt ändern und den Zeitstempel dazu. Anschließend ist von außen nicht mehr sichtbar, dass eine Veränderung der Datei vorgenommen wurde. Doch die Funktion ist von größerem Nutzen bei der Programmerstellung, wo Quellcodedateien etwa mit Objektdateien verbunden sind. Nur über einen Zeitstempel ist eine einigermaßen intelligente Projektdateiverwaltung möglich.

Dateien berühren

Unter dem UNIX-System gibt es das Shellkommando touch, welches wir in einer einfachen Variante in Java umsetzen wollen. Das Programm berührt (engl. touch) eine Datei, indem der Zeitstempel auf das aktuelle Datum gesetzt wird. Da es mit setLastModified() einfach ist, das Zeitattribut zu setzen, muss die Datei nicht geöffnet werden und etwa das erste Byte gelesen und gleich wieder geschrieben werden. Wie beim Kommando touch soll unser Java-Programm über alle auf der Kommandozeile übergebenen Dateien gehen und sie berühren. Falls eine Datei nicht existiert, soll sie kurzerhand angelegt werden. Gibt setLastModified() den Wahrheitswert false zurück, so wissen wir, dass die Operation fehlschlug und geben eine Informationsmeldung aus.

Listing 12.1   Touch.java
import java.io.*;

public class Touch
{
public static void main( String args[] ) throws IOException
{
for ( int i =
0; i < args.length; i++ )
{
File f = new File( args[i] );

if ( f.exists() )
{
boolean ok = f.setLastModified( System.currentTimeMillis() );

if ( ok )
System.out.println( "touched " + args[i] );

else
System.out.println( "touch failed on " + args[i] );
}
else
{
f.createNewFile();
System.out.println( "create new file " + args[i] );
}
}
}
}

Sicherheitsprüfung

Wir müssen uns bewusst sein, dass verschiedene Methoden, unter der Bedingung, dass ein Security-Manager die Dateioperationen überwacht, eine SecurityException auslösen können. Security-Manager kommen beispielsweise bei Applets zum Zuge. Folgende Methoden sind Kandidaten für eine SecurityException: exists(), canWrite(), canRead(), canWrite(), isDirectory(), lastModified(), length(), mkdir(), mkdirs(), list(), delete() und renameFile().


Galileo Computing

12.1.3 Umbenennen, Verzeichnisse anlegen und Datei löschen  downtop

class java.io.File
implements Serializable, Comparable

gp  boolean mkdir()
Legt das Unterverzeichnis an.
gp  boolean mkdirs()
Legt das Unterverzeichnis inklusive weiterer Verzeichnisse an.
gp  boolean renameTo( File d )
Benennt die Datei in den Namen um, der durch das File-Objekt d gegeben ist. Ging alles gut, wird true zurückgegeben.
gp  boolean delete()
true
, wenn die Datei gelöscht werden konnte. Ein zu löschendes Verzeichnis muss leer sein. Diese Methode löscht wirklich. Sie ist nicht so zu verstehen, dass sie true liefert, falls die Datei potenziell gelöscht werden kann.

Galileo Computing

12.1.4 Die Wurzel aller Verzeichnisse  downtop

Die statische Methode listRoots() gibt ein Feld von File-Objekten zurück, die eine Auflistung der Wurzeln (engl. Root) von Dateisystemen enthält. Dies macht es einfach, Programme zu schreiben, die etwa über dem Dateisystem eine Suche ausführen. Da es unter UNIX nur eine Wurzel gibt, ist der Rückgabewert von File.listRoots() immer »/« – ein anderes Root gibt es nicht. Unter Windows wird es aber zu einem richtigen Feld, da es mehrere Wurzeln für die Partitionen oder logischen Laufwerke gibt. Die Wurzeln tragen Namen wie »A:« oder »Z:«. Dynamisch eingebundene Laufwerke, die etwa unter UNIX mit mount integriert werden, oder Wechselfestplatten werden mit berücksichtigt. Die Liste wird immer dann aufgebaut, wenn listRoots() aufgerufen wird. Komplizierter ist es, wenn entfernte Dateibäume mittels NFS oder SMB eingebunden sind. Denn dann kommt es darauf an, ob das zuständige Programm eine Verbindung noch aktiv hält oder nicht. Denn nach einer abgelaufenen Zeit ohne Zugriff wird das Verzeichnis wieder aus der Liste genommen. Dies ist aber wieder sehr plattformabhängig.

class java.io.File
implements Serializable, Comparable

gp  static File[] listRoots()
Liefert die verfügbaren Wurzeln der Dateisysteme oder null, falls diese nicht festgestellt werden können. Jedes File-Objekt beschreibt eine Dateiwurzel. Es ist gewährleistet, dass alle kanonischen Pfadnamen mit einer der Wurzeln beginnen. Wurzeln, für die der SecurityManager den Zugriff verweigert, werden nicht aufgeführt. Das Feld ist leer, falls es keine Dateisystem-Wurzeln gibt.

Liste der Wurzeln ausgeben

Im folgenden Beispiel wird ein Programm vorgestellt, das mit listRoots() eine Liste der verfügbaren Wurzeln ausgibt. Dabei berücksichtigt das Programm, ob auf das Gerät eine Zugriffsmöglichkeit besteht. Unter Windows ist etwa ein Diskettenlaufwerk eingebunden, aber wenn keine Diskette im Schacht ist, so ist das Gerät nicht bereit. Das Diskettenlaufwerk taucht in der Liste auf, aber exists() liefert false.

Listing 12.2   ListRoots.java
import java.io.*;

public class ListRoots
{
public static void main( String args[] )
{
File list[] = File.listRoots();

for ( int i =
0; i < list.length; i++ )
{
File root = list[i];

if ( root.exists() )
System.out.println( root.getPath() + " bereit" );
else
System.out.println( root.getPath() + " nicht bereit" );
}
}
}

Bei der Ausgabe mit System.out.println() entspricht root.getPath() einem root.to String(). Demnach könnte das Programm etwas abgekürzt werden, etwa mit root + " XYZ". Da aber nicht unbedingt klar ist, dass toString() auf getPath() verweist, schreiben wir getPath() direkt.


Galileo Computing

12.1.5 Verzeichnisse listen und Dateien filtern  downtop

Um eine Verzeichnisanzeige zu programmieren, benötigen wir eine Liste von Dateien, die in einem Verzeichnis liegen. Ein Verzeichnis kann reine Dateien oder auch wieder Unterverzeichnisse besitzen. Die list()- und listFiles()-Funktionen der Klasse File geben ein Feld von Zeichenketten bzw. ein Feld von File-Objekten zurück.

class java.io.File
implements Serializable, Comparable

gp  String[] list()
Gibt eine Liste der Dateien zurück. Diese enthält weder ».« noch »..«.
Beispiel Ein einfacher Directory-Befehl ist somit leicht in ein paar Zeilen programmiert.
String entries[] = new File(".").list();
System.out.println( java.util.Arrays.asList(entries) );

Die einfache Funktion list() liefert dabei nur relative Pfade, also einfach den Dateinamen oder den Verzeichnisnamen. Den absoluten Namen zu einer Dateiquelle müssen wir also erst zusammensetzen. Praktischer ist da schon die Methode listFiles(), da wir hier komplette File-Objekte bekommen, die ihre ganze Pfadangabe schon kennen. Wir können den Pfad mit getName() erfragen.

Inhalt des aktuellen Verzeichnisses

Wollen wir den Inhalt des aktuellen Verzeichnisses lesen, so müssen wir irgendwie an den absoluten Pfad kommen, um damit das File-Objekt zu erzeugen. Dazu müssten wir über die System-Properties gehen. Die System-Properties verwalten die systemabhängigen Variablen, unter anderem auch den Pfadseparator und das aktuelle Verzeichnis.

final class java.lang.System

gp  String getProperty( String )
Holt den bestimmten Property-Eintrag für ein Element.

Über die System-Properties und den Eintrag »user.dir« gelangen wir ans aktuelle Benutzerverzeichnis und list() wird wie folgt ausgewertet:

File userdir = new File( System.getProperty("user.dir") 
);
String entries[] = userdir.list( new FileFilter() );

for ( int i = 0; i < entries.length; i++ )
System.out.println( entries[i] );

Wenn wir etwas später den grafischen Dateiselektor kennen lernen, so können wir dort auch den FilenameFilter einsetzen. Leider ließ der Fehlerteufel seine Finger nicht aus dem Spiel und der FilenameFilter funktioniert nicht, da der FileSelector fehlerhaft ist.

Dateien nach Kriterien filtern mit FilenameFilter

Sollen aus einer Liste von Dateien einige mit bestimmten Eigenschaften (zum Beispiel der Endung) herausgenommen werden, so müssen wir dies nicht selbst programmieren. Schlüssel hierzu ist die Schnittstelle FilenameFilter. Es filtert aus den Dateinamen diejenigen heraus, die einem gesetzten Kriterium genügen oder nicht. Die einfachste Möglichkeit ist, nach den Endungen zu separieren. Doch auch komplexere Selektionen sind denkbar; so kann in die Datei hineingeschaut werden, ob sie beispielsweise bestimmte Informationen am Dateianfang enthält. Besonders für Macintosh-Benutzer ist dies wichtig zu wissen, denn dort sind die Dateien nicht nach Endungen sortiert. Die Information liegt in den Dateien selber. Windows versucht uns auch diese Dateitypen vorzuenthalten, aber von dieser Kennung hängt alles ab. Wer die Endung einer Grafikdatei schon einmal umbenannt hat, der weiß, warum Grafikprogramme aufgerufen werden. Von den Endungen hängt also sehr viel ab.

class java.io.File
implements Serializable, Comparable

gp  public String[] list( FilenameFilter )
Wie list(), nur filtert ein spezielles FilenameFilter-Objekt bestimmte Namen heraus.
interface java.io.FilenameFilter

gp  boolean accept( File verzeichnis, String dateiname )
Testet, ob dateiname in verzeichnis vorkommt. Gibt true zurück, wenn dies der Fall ist.

Eine Filter-Klasse implementiert die Funktion accept() von FilenameFilter so, dass alle angenommenen Dateien den Rückgabewert true liefern. Wollen wie nur auf Textdateien reagieren, so geben wir ein true bei allen Dateien mit der Endung .txt zurück. Die anderen werden mit false abgelehnt.

class FileFilter implements FilenameFilter
{
public boolean accept( File f, String s )
{
if ( s.toLowerCase().endsWith(".txt") )
return true;

return false;
}
}
Abbildung

Nun kann list() mit dem FilenameFilter aufgerufen werden. Wir bekommen eine Liste mit Dateinamen, die wir in einer Schleife einfach ausgeben. An dieser Stelle merken wir schon, dass wir nur für FilenameFilter eine neue Klasse schreiben müssen. An dieser Stelle bietet sich wieder eine innere Klasse an. Zusammen mit list() ergibt sich dann folgender Programmcode, um alle Verzeichnisse auszufiltern:

String a[] = entries.list( new 
FilenameFilter() {
public boolean accept( File d, String name ) {
return d.isDir();
} } );

Die einfache Implementierung von list() mit FilenameFilter

Die Methode list() holt zunächst ein Feld von Dateinamen ein. Nun wird jede Datei mittels der accept()-Methode geprüft und in eine interne Liste übernommen. Zum Einsatz kommt hier eine Liste aus der 1.2 Collection API, die Klasse ArrayList. Nach dem Testen jeder Datei wird das Array in ein Feld von String konvertiert ((String[])(v.toArray(new String[0])).

Dateien aus dem aktuellen Verzeichnis filtern

Wir können somit ein einfaches Verzeichnisprogramm programmieren, indem wir die Funktionen von getProperty() und list() zusammenfügen. Zusätzlich wollen wir nur Dateien mit der Endung .txt angezeigt bekommen.

Listing 12.3   Dir.java
import java.io.*;

class FileFilter implements FilenameFilter
{
public boolean accept( File f, String s )
{
if ( s.toLowerCase().endsWith(".txt") )
return true;

return false;
}
}

class Dir
{
public static void main( String args[] )
{
File userdir = new File( System.getProperty("user.dir") );
System.out.println( userdir );

String entries[] = userdir.list( new FileFilter() );

for ( int i = 0; i < entries.length; i++ )
System.out.println( entries[i] );
}
}

Liste von File-Objekten in einer dynamischen Datenstruktur

Wollen wir alle Dateien eines Verzeichnisses in einer Liste verwalten, so müssen wir erst einen FileFilter programmieren, dann das Feld noch einmal durchlaufen und anschließend die Liste aufbauen. Der Filter lässt sich auch gut dafür gebrauchen, gleich die Liste aufzubauen. Dazu interessieren wir uns einfach nicht für den Rückgabewert von listFiles(), sondern fügen die in der accept()-Methode übergebenen File-Objekte gleich in die Liste ein.

final ArrayList list = new ArrayList( 
512 );
new File( path ).listFiles(
new FileFilter() {
public boolean accept( File f ) {
if ( f.isFile() ) {
list.add(f);
return true;
}
return false;
}
}
);

Collections.shuffle( list );

Wir haben das Problem mit einer inneren Klasse gelöst, da wir für den Filter nur eine kleine Methode programmieren müssen. Die einzige Besonderheit liegt in der Liste list. Sie ist lokal für die Methode, und da die innere Klasse nicht auf einfache lokale Variablen zugreifen kann, muss sie final sein. Dann kopiert der Compiler automatisch die Referenz auf die LinkedList in die innere Klasse. Die letzte Zeile zeigt für Collection-Objekte – und List ist eine Collection – was es für Felder nicht gibt: durcheinander würfeln der Einträge. Mit Collections.sort() können wir auch sortieren. Für Felder gibt es Arrays.sort().


Galileo Computing

12.1.6 Implementierungsmöglichkeiten für die Klasse File  downtop

Wir sollten für die Klasse File noch einmal festhalten, dass sie lediglich ein Verzeichnis oder eine Datei im Verzeichnissystem repräsentiert, jedoch keine Möglichkeit bietet, auf die Daten selbst zuzugreifen. Es ist ebenso offensichtlich, dass wir mit den plattformunabhängigen Eigenschaften von Java spätestens bei Zugriffen auf das Dateisystem nicht mehr weiter kommen. Wenn zum Beispiel eine Datei gelöscht werden soll oder wir eine Liste von Dateien im Pfad erbeten, sind Betriebssystemfunktionen mit von der Partie. Diese sind dann nativ programmiert. Es bieten sich zwei Möglichkeiten für die Implementierung an. Zunächst ist es denkbar, die nativen Methoden in der Klasse File selbst anzugeben. Diesen Weg ging die Sun-Implementierung eine ganze Zeit lang und Kaffe nutzt diese Variante heute noch. Doch die Verschmelzung von einem Dateisystem in der Klasse File bietet auch Nachteile. Was ist, wenn das Dateisystem eine Datenbank ist, sodass die typischen nativen C-Funktionen unpassend sind? Aus dieser Beschränkung heraus hat sich Sun dazu entschlossen eine nur paketsichtbare, abstrakte Klasse FileSystem einzufügen, die ein abstraktes Dateisystem repräsentiert. Das Betriebssystem implementiert folglich eine konkrete Unterklasse für FileSystem, die dann von File genutzt werden kann. File ist dann völlig frei von nativen Methoden und leitet alles an das FileSystem-Objekt weiter, das intern mit folgender Zeile angelegt wird:

static private FileSystem fs = 
FileSystem.getFileSystem();

Dann steht zum Beispiel für den Zugriff auf die Länge einer Datei:

public long length() {
SecurityManager security = System.getSecurityManager();
if (security != null)
security.checkRead(path);
return fs.getLength(this);
}

Wenn wir dies noch einmal mit dem ersten Weg vergleichen, dann finden wir in der File-Implementierung von Kaffe etwa Folgendes:

public class File implements Serializable, 
Comparable
{
static {
System.loadLibrary("io");
}

public long length() {
checkReadAccess();
return length0();
}

native private long length0();
}

Die native Methode ist selbstverständlich privat. Oft trägt sie die Endung 0, sodass eine Unterscheidung einfach ist.

Um das Geheimnis um die native Methode length0() zu lüften und einen Eindruck von nativen Methoden zu vermitteln, gönnen wir uns einen Blick auf die Implementierung:

jlong
java_io_File_length0(struct Hjava_io_File* this)
{
struct stat buf;
char str[MAXPATHLEN];
int r;

stringJava ist auch eine InselCBuf(unhand(this)->path, str, sizeof(str));

r = KSTAT(str, &buf);
if (r != 0)
return ((jlong)0);

return ((jlong)buf.st_size);
}

Der Aufruf stringJava ist auch eine InselCBuf() konvertiert die Unicode-Zeichenfolge in eine gültige C–Zeichenkette, die mit einem Null-Byte abschließt. Jetzt folgt nur noch der Aufruf einer ANSI-Bibliotheksfunktion, die noch einmal über KSTAT gekapselt ist.

Wenn wir uns etwas später mit dem tatsächlichen Zugriff beschäftigen, dann benutzt auch diese Klasse native Methoden und eine abstrakte Repräsentation eines Dateideskriptors.


Galileo Computing

12.1.7 Verzeichnisse nach Dateien rekursiv durchsuchen  downtop

Im vorausgehenden Kapitel haben wir einige Datenstrukturen kennen gelernt, unter anderem Vector und Stack. Wir wollen damit ein Programm formulieren, welches rekursiv die Verzeichnisse durchläuft und nach Dateien durchsucht. Die Vektor-Klasse dient dazu, die Dateien zu speichern, und mit dem Stack merken wir uns die jeweiligen Verzeichnisse (Tiefensuche), in die wir absteigen.

Listing 12.4   FileFinder.java
import java.io.*;
import java.util.*;
public class FileFinder
{
public static void main( String args[] )
{
String suffix[] = { ".gif", ".jpg", ".tif" } ;

String userDir = System.getProperty("user.dir"),
path = new File(userDir).getParent();

System.out.println( "Looking in path:" + path );

FileFinder ff = new FileFinder( path, suffix );
ff.print();
}

public void print()
{
int noFiles = files.size();

if ( noFiles == 0 ) {
System.out.println( "No files found." );
return;
}

System.out.print( "Found " + noFiles + " file" );

if ( noFiles != 1 )
System.out.println( "s." );
else
System.out.println( "." );

for ( int i = 0; i < noFiles; i++ )
{
String path;
path = ((File)files.elementAt(i)).getAbsolutePath();
System.out.println( path );
}
}

public FileFinder( String start, String extensions[] )
{
files = new Vector();
Stack dirs = new Stack();

File startdir = new File(start);

if ( startdir.isDirectory() )
dirs.push( new File(start) );

while ( dirs.size() > 0 )
{
File dirFiles = (File) dirs.pop();

String s[] = dirFiles.list();
if ( s != null )
{
for ( int i = 0; i < s.length; i++ ) {
File file = new File( dirFiles.getAbsolutePath()
+ File.separator + s[i] );
if ( file.isDirectory() )
dirs.push( file );
else
if ( match(s[i], extensions) )
files.addElement( file );
}
}
}
}

private static boolean match( String s1, String suffixes[] )
{
for (int i = 0; i < suffixes.length; i++)
if ( s1.length() >= suffixes[i].length() &&
s1.substring(s1.length() – suffixes[i].length(),
s1.length()).equalsIgnoreCase(suffixes[i]))
return true;

return false;
}

private Vector files;
}

Galileo Computing

12.2 Dateien mit wahlfreiem Zugriff  downtop

Dateien können auf zwei unterschiedliche Arten und Weisen gelesen und modifiziert werden. Zum einen über einen Datenstrom, der Bytes wie in einem Medienstream nimmt oder wie ein Programm, welches eine neue Datei aufbaut, in dem es Daten in einen Datenstrom schreibt, zum anderen über einen wahlfreien Zugriff (engl. Random Access) – also nicht über einen Datenstrom, der eine strenge Sequenz erzwingt. Die andere Variante ist nicht über einen Datenstrom, der eine strenge Sequenz erzwingt, nämlich wahlfreien Zugriff, im Englischen Random Access genannt. »Wahlfrei« deshalb, da innerhalb der Datei beliebig hin und her gesprungen werden kann und ein Dateizeiger verwaltet wird, den wir anpassen können. Da wir es mit Dateien zu tun haben, heißt das Ganze dann Random Access File und die Klasse, die wahlfreien Zugriff anbietet, java.io.RandomAccessFile.


Galileo Computing

12.2.1 Eine RandomAccessFile öffnen  downtop

Die Klasse definiert zwei Konstuktoren, um mit einem Dateinamen oder File-Objekt ein RandomAccessFile-Objekt anzulegen. Im Konstruktor ist der zweite Parameter eine Zeichenkette für den Zugriff. Damit lässt sich eine Datei lesen oder schreibend öffnen. Die Angabe vermeidet Fehler, da nicht aus versehen eine zum Lesen geöffnete Datei überschrieben werden kann.

Die Modi bedeuten Folgendes:

r Die Datei wird zum Lesen geöffnet. Wenn sie nicht vorhanden ist, wird ein Fehler ausgelöst. Der Versuch auf diese Datei schreibend zuzugreifen wird mit einer Exception bestraft.
rw Die Datei wird zum Lesen oder Schreiben geöffnet. Eine existierende Datei wird dabei geöffnet und hinten können die Daten angehängt werden, ohne dass die Datei gelöscht wird. Existiert die Datei nicht, wird sie neu angelegt und ihre Startgröße ist Null. Soll die Datei gelöscht werden, so müssen wir dies ausdrücklich selbst tun, in dem wir etwa delete() aufrufen.

class java.io.RandomAccessFile
implements DataOutput, DataInput

gp  public RandomAccessFile( String name, String mode )

Öffnet die Datei. Ob aus der Datei gelesen wird oder die Datei geschrieben wird, bestimmt der String, der den Modus angibt. »r« oder »rw« sind erlaubt. Ist der Modus falsch gesetzt, zeigt eine IllegalArgumentException dies an. Vor dem Öffnen der Datei wird geprüft, ob die erforderlichen Zugriffsrechte für die Datei vorhanden sind. Eine ausgelöste SecurityException zeigt fehlende Schreib- oder Leserechte an.

gp  public RandomAccessFile( File file, String mode )

Erzeugt einen Random-Access-Dateistrom vom File-Objekt. Löst eine FileNotFoundException aus, falls die Datei nicht geöffnet werden kann. Eine IllegalArgumentException signalisiert, dass der Modus falsch ist.

Einer der Konstruktoren öffnete die Datei. Sie wird mit close() wieder geschlossen.


Galileo Computing

12.2.2 Aus dem RandomAccessFile lesen  downtop

Um Daten aus einer mit einem RandomAccessFile verwalteten Datei zu bekommen, nutzen wir eine der readXXX()-Methoden. Sie lesen direkt den primitiven Datentyp oder das Bytefeld aus der Datei, wo es im Binärformat abgelegt ist.

Da jede der Methoden eine IOException im Fehlerfall auslöst, ist dies in der Beschreibung nicht extra aufgeführt. Liegen keine Daten mehr am Datenstrom an, da das Ende der Datei erreicht ist, aber dennoch ein Leseversuch gemacht wird, so ist der Rückgabewert –1. Alle Lesemethoden sind blockiert, das heißt, sie warten so lange, bis Daten ankommen oder die Daten zu Ende sind.

class java.io.RandomAccessFile
implements DataOutput, DataInput

gp  native int read()
Liest genau ein Byte und liefert es als int zurück.
gp  int read( byte[] b )
Liest b.length()-Bytes und speichert sie im Feld b.
gp  int read( byte[] b, int off, int len )
Liest len Bytes aus der Datei und schreibt sie in das Feld b ab der Position off. Konnten mehr als ein Byte gelesen werden, aber wengier als len, dann wird die gelesene Größe als Rückgabewert zurückgegeben.
gp  final boolean readBoolean()
Liest einen boolean-Wert.
gp  final byte readByte()
Liest einen byte-Wert.
gp  final char readChar()
Liest einen char-Wert.
gp  final double readDouble()
Liest ein double.
gp  final float readFloat()
Liest ein float.
gp  final void readFully( byte b[] )
Versucht, den gesamten Puffer b zu füllen.
gp  final void readFully( byte b[], int off, int len )
Liest len Bytes und speichert sie im Puffer b ab dem Index off.
gp  final int readInt()
Liest ein int.
gp  final long readLong()
Liest ein long.
gp  final short readShort()
Liest ein short.
gp  final int readUnsignedByte()
Liest ein als vorzeichenlos interpretiertes Byte.
gp  final int readUnsignedShort()
Liest zwei als vorzeichenlos interpretierte Bytes.
gp  Zum Schluss bleiben zwei Methoden, die eine Zeichenkette liefern.
gp  final String readLine()
Liest eine Textzeile. Als Zeilenende wird \n und \r\n akzeptiert. Aus der Datei werden die Bytes einfach als ASCII-Bytes genommen und nicht als Unicode interpretiert. Die Methode nimmt keine Umwandlung verschiedener Codepages vor, sodass korrekte Unicode-Zeilen einer Umgebung herauskommen. Diese Umwandlung müsste manuell gemacht werden.
gp  final String readUTF()
Liest einen UTF-codierten String und gibt einen Unicode-String zurück. Bei UTF-Strings werden entweder 1, 2 oder 3 Bytes zu einem Unicode-Zeichen zusammengefasst.

Galileo Computing

12.2.3 Hin und her in der Datei  downtop

Die bisherigen Lesemethoden setzen die Datenzeiger automatisch eine Position weiter. Wir können jedoch auch den Datenzeiger manuell auf eine selbstgewählte Stelle setzen und damit durch die Datei navigieren. Die nachfolgenden Lese- oder Schreibzugriffe setzten dann dort an. Die im Folgenden beschriebenen Methoden haben etwas mit diesem Dateizeiger und seiner Position zu tun:

class java.io.RandomAccessFile
implements DataOutput, DataInput

gp  native long getFilePointer()
Liefert die momentane Position des Dateizeigers. Das erste Byte steht an der Stelle Null. Da der Rückgabewert long ist und nicht BigInteger, ist die maximale Dateilänge auf 2 GB begrenzt.
gp  native long length()
Liefert die Größe der Datei in Bytes.
gp  native void seek( long pos )
Setzt den Position des Dateizeiger auf pos. Diese Angabe ist absolut und kann daher nicht negativ sein. Falls doch, wird eine Ausnahme ausgelöst.
gp  int skipBytes( int n )
Mit skipBytes() kann im Gegensatz zu seek() relativ positioniert werden. n ist die Anzahl, um die der Dateizeiger bewegt wird. Ein negativer Wert setzt den Zeiger nach vorne. Falls versucht wird, den Zeiger vor die Datei zu setzen, wird eine IOException ausgelöst.

Setzten seek() oder skipBytes() weiter als es möglich ist, dann wird die Datei dadurch nicht größer. Sie verändert aber dann ihre Größe, wenn Daten geschrieben werden.


Galileo Computing

12.2.4 Die Länge des RandomAccessFile  downtop

Mit zwei Methoden greifen wir auf die Länge der Datei zu. Einmal schreibend verändernd und einmal lesend.

gp  native void setLength( long newLength )
Setzt die Größe der Datei auf newLength. Ist die Datei kleiner als newLength, wird sie mit unbestimmten Daten vergrößert; wenn die Datei größer war als die zu setzende Länge, wird die Datei abgeschnitten. Das bedeutet, mit setLength(0) ist der Dateiinhalt leicht zu löschen.
gp  native long length()
Liefert die Länge der Datei. Schreibzugriffe erhöhen den Wert und setLength() modifiziert ebenfalls die Länge.

Da RandomAccessFile die Schnittstellen DataOutput und DataInput implementiert, werden zum einen die readXXX()-Methoden wie bisher vorgestellt implementiert, und zum anderen eine Reihe von Schreibmethoden der Form writeXXX(). Diese sind orthogonal den Lesefunktionen, sodass sie hier nicht weiter erklärt werden. Wir listen sie lediglich kurz auf: void write(byte b[]), void write(int b), void write(byte b[], int off, int len), void writeBoolean(boolean v), void writeByte(int v), void writeBytes(String s), void writeChar(int v), void writeChars(String s), void writeDouble(double v), void writeFloat(float v), void writeInt(int v), void writeLong(long v), void writeShort(int v), void writeUTF(String str).

Beispiel Wir hängen an eine Datei etwas an. Listing 12.5   FileAppend.java
import java.io.*;

public class FileAppend
{
public static void main( String args[] )
{
if ( args.length != 2 )
{
System.out.println( "Aufruf: FileAppend string outfile" );
System.exit( 1 );
}

RandomAccessFile output = null;

try
{
output = new RandomAccessFile( args[1], "rw" );
output.seek( output.length() ); // Dateizeiger an das Ende
output.writeChars( args[0] + "\n" ); // Zeile schreiben
}
catch ( Exception e ) {
System.err.println( e );
}
}
}


Galileo Computing

12.3 Übersicht über wichtige Stream- und Writer/Reader  downtop

Alle Datenströme sind als Klassen repräsentiert, die im Paket java.io gesammelt sind. In den ersten Java-Versionen konnten nur Primitive bzw. Bytes und Byte-Arrays geschrieben und gelesen werden. Doch seit Java 1.1 gibt es für Byte-Ströme und Unicodezeichen-Ströme getrennte Klassen. Das macht aus mehreren Gründen Sinn: Das Einlesen von Unicode-Dateien ist vereinfacht und Daten müssen nicht auf festgelegten Zeichensätzen arbeiten, ja wir bekommen vom Konvertieren von Unicode nach Bytes überhaupt nichts mit. Ein anderer Vorteil ist die gewonnene Einlesegeschwindigkeit. Die meisten Byte-Ströme lesen und schreiben nur dann ein Zeichen, wenn sie es brauchen.

Die folgenden Tabellen geben eine Übersicht über die wichtigsten Eingabe- und Ausgabe-Klassen.

Tabelle 12.1   Wichtige Eingabeklassen
Byte-Stream-Klasse für
die Eingabe
Zeichen-Stream-Klasse
für die Eingabe
Beschreibung
InputStream Reader Abstrakte Klasse für Zeichen-Eingabe und Byte-Arrays
BufferedInputStream BufferedReader Puffert die Eingabe
LineNumberInputStream LineNumberReader Merkt sich Zeilennummern beim Lesen
ByteArrayInputStream CharArrayReader Liest Zeichen-Arrays oder Byte-Arrays
(keine Entsprechung) InputStreamReader Wandelt Byte-Stream in Zeichen-Stream um, Bindeglied zwischen Bytes und Zeichen
DataInputStream (keine Entsprechung) Liest Primitive und auch UTF-8
FilterInputStream FilterReader Abstrakte Klassse für gefilterte Eingabe
PushbackInputStream PushbackReader Erlaubt gelesene Zeichen wieder in den Strom zu geben
PipedInputStream PipedReader Liest von einem
PipedWriter oder
PipedOutputStream
StringBufferInputStream    
unter 1.1 nicht mehr erwünscht StringReader Liest aus Strings
SequenceInputStream (keine Entsprechung) Verbindet zwei andere InputStreams
TelepathicInputStream TelepathicWriter Überträgt Daten mittels Telepathie

Tabelle 12.2   Wichtige Ausgabeklassen
Byte-Stream-Klasse für die
Ausgabe
Zeichen-Stream Klasse für die Ausgabe Beschreibung
OutputStream Writer Abstrakte Klasse für Zeichenausgabe oder Byte-Ausgabe
BufferedOutputStream BufferedWriter Ausgabe des Puffers, nutzt passendes Zeilenendezeichen
ByteArrayOutputStream CharArrayWriter Schreibt Arrays
DataOutputStream (kein Entsprechung) Schreibt Primitive und auch UTF-8
(keine Entsprechung) OutputStreamWrit er Übersetzt Zeichen-Stream in Byte-Stream
FileOutputStream FileWriter Übersetzt Zeichen-Stream in eine Byte-Datei
PrintStream PrintWriter Schreibt Werte (oder Objekte)
PipedOutputStream PipedWriter Schreibt in eine Pipe
(keine) StringWriter Schreibt einen String

Neben diesen aufgeführten Klassen zur dateiorientierten Ein- und Ausgabe existiert noch eine Klasse StreamTokenizer, die auf elegante Art die Aufspaltung des Eingabestroms in Token erlaubt.


Galileo Computing

12.3.1 Die abstrakten Basisklassen  downtop

Eine häufig eingesetzte Methode in Java ist die Erweiterung von abstrakten Klassen zwecks Definition von Schnittstellen. Auch komplexe Eingabeklassen wie FileInputStream, ByteArrayOutputStream oder BufferedWriter basieren auf einer abstrakte Klasse. Diese können wir in vier Kategorien einteilen. Klassen zur Ein- und Ausgabe von Bytes (oder Byte-Arrays) und Unicode Zeichen (oder Strings), so wie wir es oben motiviert haben. Die aufgeführten Klassen geben Methoden vor, die für jede implementierende Klasse von Interesse ist.

Klasse für Eingabe Ausgabe
Bytes (oder Byte-Arrays) InputStream OutputStream
Zeichen (oder Strings) Reader Writer


Galileo Computing

12.4 Eingabe- und Ausgabe-Klassen: InputStream und OutputStream  downtop

Wir müssen auf InputStream und OutputStream ein besonderes Auge werfen, da diese beiden Klassen von Bedeutung sind. Diese Klassen bilden die Grundlage für alle anderen Klassen und dienen somit als Bindeglied bei Funktionen, die als Parameter ein Eingabe- und Ausgabe-Objekt verlangen. So ist ein InputStream nicht nur für Dateien denkbar, sondern auch für Daten, die über das Netzwerk kommen.


Galileo Computing

12.4.1 Die Klasse OutputStream  downtop

Der Clou bei allen Datenströmen ist nun, dass spezielle Unterklassen wissen, wie sie genau die vorgeschriebene Funktionalität implementieren. Wenn wir uns den OutputStream anschauen, dann sehen wir auf den ersten Blick, dass hier alle wesentlichen Operationen um das Schreiben versammelt sind. Das heißt, dass ein Strom, der in Dateien schreibt, nun weiß, wie er Bytes in Dateien schreiben wird. Natürlich ist hier auch Java mit seiner Plattformunabhängigkeit am Ende und es werden native Methoden eingesetzt.

Abbildung

abstract class java.io.OutputStream

gp  abstract write( int )
Schreibt ein einzelnes Byte in den Strom.
gp  write( byte [] )
Schreibt die Bytes aus Array in den Strom.
gp  write( byte [], int o, int l )
Liest l Byte ab Position o aus dem Array und schreibt ihn in den Ausgabestrom.
gp  void flush()
Gepufferte Daten werden geschrieben.
gp  void close()
Schließt den Datenstrom.

Zwei Eigenschaften lassen sich an den Methoden ablesen: Einmal, dass nur Bytes geschrieben werden und einmal, dass nicht wirklich alle Methoden abstract sind. Zur ersten Eigenschaft: Wenn nur Bytes geschrieben werden, dann bedeutet es, dass andere Klassen diese erweitern können, denn eine Ganzzahl ist nichts anderes als mehrere Bytes in einer geordneten Folge.

Nicht alle diese Methoden sind wirklich elementar, müssen also nicht von allen Ausgabeströmen überschrieben werden. Wir entdecken, dass nur write(int) abstrakt ist. Das hieße aber, alle anderen wären konkret. Im gleichen Moment stellt sich die Frage, wie denn ein OutputStream, das die Eigenschaften für alle erdenklichen Ausgabeströme vorschreibt, wissen kann, wie denn ein spezieller Ausgabestrom etwa geschlossen (close()) wird oder seine gepufferten Bytes schreibt (flush()). Das weiss er natürlich nicht, aber die Entwickler haben sich dazu entschlossen, eine leere Implementierung anzugeben. Der Vorteil liegt darin, dass Programmierer von Unterklassen nicht verpflichtet werden immer die Methoden zu überschreiben, auch wenn wir sie gar nicht nutzen wollen.

Es fällt auf, dass es zwar drei Schreibmethoden gibt, aber nur eine davon wirklich abstrakt ist. Das ist trickreich, denn tatsächlich lassen sich die Methoden, die ein Bytefeld schreiben, auf die Methode, die ein einzelnes Byte schreibt, abbilden. Wir schauen in den Quellcode der Bibliothek:

public void write(byte b[]) throws 
IOException {
write(b, 0, b.length);
}

public void
write(byte b[], int off, int len) throws IOException {
if (b == null)
throw new NullPointerException();
else if ((off < 0) || (off > b.length) || (len < 0) ||
((off + len) > b.length) || ((off + len) < 0)) {
throw new IndexOutOfBoundsException();
} else if (len == 0)
return;
for (int i = 0 ; i < len ; i++)
write(b[off + i]);
}

An beiden Implementierungen ist zu erkennen, dass sie sehr bequem an andere Methoden die Arbeit verschieben. Doch in einer Schleife ist write() aufwändig, denn die Methode testet immer wieder den Offset und die Länge der Parameter.

Stellen wir uns vor, ein Dateiausgabestrom überschreibt nur die eine abstrakte Methode, die nötig ist. Und nehmen wir weiterhin an, dass unser Programm nun immer ganze Bytefelder schreibt, etwa eine 5 MB Datei, die im Speicher steht. Dann wird für jedes Byte im Byte-Array in einer Schleife alle Bytes der Reihe nach an einer vermutlich nativen Methoden übergeben. Wenn es so implementiert wäre, könnten wir die Geschwindigkeit des Mediums überhaupt nicht nutzen, zumal jedes Dateisystem Funktionen bereitstellt, mit dem sich ganze Blöcke übertragen lassen. Glücklicherweise sieht die Implementierung nicht so aus, denn wir haben in dem Modell vergessen, dass die Unterklassen zwar die abstrakte Methode implementieren muss, aber immer noch andere Methoden überschreiben kann. Ein späterer Blick auf die Klasse FileOutputStream bestätigt das.


Galileo Computing

12.4.2 Ein Datenschlucker  downtop

Damit wir sehen können, wie alle Unterklassen prinzipiell mit OutputStream umgehen, wollen wir eine Klasse entwerfen, die alle Daten verwirft, die ihr gesendet werden. Die Klasse ist vergleichbar mit dem Unix-Device /dev/null. Die Implementierung ist die Einfachste, die sich denken lässt, denn alle write()-Methoden machen nichts.

Listing 12.6   NullOutputStream.java
import java.io.*;

public final class NullOutputStream extends OutputStream
{
public void write( byte b[] ) {}
public void write( byte b[], int off, int len ) {}
public void write( int b ) throws IOException {}
}

Da close() und flush() sowieso schon mit einem leeren Block implementiert sind, brauchen wir diese nicht noch einmal zu überschreiben. Aus Effizienzgründen (!) geben wir auch eine Implementierung für die Schreib-Feld-Methoden an.


Galileo Computing

12.4.3 Die Eingabeklasse InputStream  downtop

Ein Eingabestrom wird durch die abstrakte Klasse InputStream repräsentiert.

Abbildung

abstract class java.io.InputStream

gp  boolean markSupported()
Gibt einen Wahrheitswert zurück, ob der Datenstrom das Merken und Zurücksetzen von Positionen gestattet. Diese Markierung ist ein Zeiger, der auf bestimmte Stellen in der Eingabedatei zeigen kann und dann entsprechend der Position die Daten ausliest. Nur ein RandomAccessFile darf den Dateizeiger manipulieren. Ein Eingabestrom kann keinen Merker setzen.
gp  int available()
Gibt die Anzahl der Zeichen im Strom zurück, die sofort ohne Blockierung gelesen werden können.
gp  int read()
Liest ein Byte als Integer aus dem Datenstrom. Ist das Ende des Datenstroms erreicht wird -1 übergeben. Die Funktion ist überladen wie die nächsten Signaturen zeigen.
gp  int read( byte [] )
Mehrere Bytes werden in ein Feld gelesen. Die tatsächliche Länge der gelesenen Bytes wird zurückgegeben.
gp  int read( byte [], int off, int len )
Liest den Datenstrom in ein Bytefeld, schreibt ihn aber erst an der Stelle off in das Bytefeld. Zudem begrenzt len die maximale Anzahl von zu lesenden Zeichen.
gp  long skip( long )
Überspringt eine Anzahl von Zeichen.
gp  mark( int )
Merkt sich eine Position im Datenstrom.
gp  void close()
Schließt den Datenstrom.
gp  void reset()
Springt wieder zurück zur Position, die mit mark() gesetzt wurde.

Gelingt eine Operation nicht, bekommen wir eine IOException.


Galileo Computing

12.4.4 Anwenden der Klasse FileInputStream  downtop

Bisher haben wir die grundlegenden Ideen der Strom-Klassen kennen gelernt, aber noch kein echtes Beispiel. Dies soll sich nun ändern. Wir wollen für einfache Dateieingaben die Klasse FileInputStream verwenden (FileInputStream implementiert InputStream). Wir binden mit dieser Klasse eine Datei (ein Objekt vom Typ File) an einen Datenstrom.

class java.io.FileInputStream
extends InputStream

Um ein Objekt anzulegen, haben wir die Auswahl zwischen drei Konstruktoren:

gp  FileInputStream( String )
Erzeugt einen FileInputStream mit einem gegebenen Dateinamen.
gp  FileInputStream( File )
Erzeugt FileInputStream aus einem File-Objekt.
gp  FileInputStream( FileDescriptor )
Erzeugt FileInputStream aus einem FileDescriptor-Objekt.
Abbildung

Ein Programm, welches seinen eigenen Quellcode anzeigt, sieht wie folgt aus:

Listing 12.7   ReadQuellcode.java
import java.io.*;

class ReadQuellcode
{
public static void main( String args[] )
{
byte buffer[] = new byte[4000];

try {
FileInputStream in;
in = new
FileInputStream( "ReadQuellcode.java" );

int len = in.
read( buffer, 0, 4000 );
String str = new String( buffer, 0, len );

System.out.println( str );
}
catch ( Exception e ) {
System.out.println( e.toString() );
}
}
}

Das Programm ist bewusst einfach gehalten und berücksichtigt keine Programme, die größer als 4 KB sind.


Galileo Computing

12.4.5 Anwendung der Klasse FileOutputStream  downtop

Das Gegenstück zu FileInputStream ist FileOutputStream. Diese einfache Klasse bietet grundlegende Schreibmethoden, die OutputStream für Dateien vorschreibt.

class java.io.FileOutputStream
extends OutputStream

Die Klasse hat drei wichtige Konstruktoren:

gp  FileOutputStream( String )
Erzeugt einen FileOutputStream mit einem gegebenen Dateinamen.
gp  FileOutputStream( File )
Erzeugt FileOutputStream aus einem File-Objekt.
gp  FileOutputStream(FileDescriptor)
Erzeugt FileOutputStream aus einem FileDescriptor-Objekt.

Das nachfolgende Programm liest eine Eingabezeile und schreibt diese in eine Datei:

Listing 12.8   TestReadWrite.java
import java.io.*;

class TestReadWrite
{
public static void main( String args[] )
{
byte buffer[] = new byte[80];

try
{
System.out.println( "\nGib eine nette Zeile ein:" );
int bytes = System.in.read( buffer );

FileOutputStream fileOut =
new FileOutputStream("line.txt");

fileOut.write( buffer, 0, bytes );
fileOut.
close();
}
catch ( Exception e ) { System.out.println(e); }
}
}

Galileo Computing

12.4.6 Kopieren von Dateien  downtop

Als Beispiel für das Zusammenspiel von FileInputStream und FileOutputStream wollen wir nun ein Datei-Kopierprogramm entwerfen. Es ist einleuchtend, dass wir zunächst die Quelldatei öffnen müssen. Taucht ein Fehler auf, wird dieser zusammen mit allen anderen Fehlern in einer besonderen IOException-Fehlerbehandlung ausgegeben. Wir trennen hier die Fehler nicht besonders. Nach dem Öffnen der Quelle wird eine neue Datei angelegt. Das machen wir einfach mit FileOutputStream. Der Methode ist es jedoch ziemlich egal, ob es schon eine Datei mit diesen Namen gibt, da sie diese gnadenlos überschreibt. Auch darum kümmern wir uns nicht. Wollten wir das berücksichtigen, sollten wir mithilfe der File-Klasse die Existenz einer Datei mit dem gleichen Namen prüfen. Doch wenn alles glatt geht, lassen sich die Bytes kopieren. Der naive und einfachste Weg liest jeweils ein Byte ein und schreibt dieses.

Es muss nicht extra erwähnt werden, dass die Geschwindigkeit dieses Ansatzes erbärmlich ist. Das Puffern in einen BufferedInputStream bzw. Ausgabestrom ist in diesem Falle unnötig, da wir einfach einen Puffer mit read(byte[]) füllen können. Da diese Methode die Anzahl tatsächlich gelesener Bytes zurückliefert, schreiben wir diese direkt mittels write() in den Ausgabepuffer. Hier bringt eine Pufferung über eine Puffer-Klasse keine zusätzliche Geschwindigkeit ein, da wir ja selbst einen 64 k Puffer einrichten.

Listing 12.9   FileCopy.java
import java.io.*;

public class FileCopy
{
static void copy( String src, String dest )
{
try
{
InputStream fis = new FileInputStream( src );
OutputStream fos = new FileOutputStream( dest );

byte buffer[] = new byte[0xffff];
int nbytes;

while ( (nbytes = fis.read(buffer)) != -1 )
fos.write( buffer, 0, nbytes );

fis.close();
fos.close();
}
catch( IOException e ) {
System.err.println( e );
}
}

public static void main( String args[] )
{
if ( args.length < 2 )
System.out.println( "Usage: java FileCopy <src> <dest>" );
else
copy( args[0], args[1] );
}
}

Galileo Computing

12.4.7 Daten filtern durch FilterInputStream und FilterOutputStream  downtop

Die Daten, die durch irgendwelche Kanäle zum Benutzer kommen, können durch zwei spezielle Klassen gefiltert werden, durch FilterInputStream und FilterOutputStream. Die Klassen erweitern entweder InputStream oder OutputStream und dienen als Basis für eine ganze Reihe von Klassen, beispielsweise ist der FilterInputStream die Basis für BufferedInputStream, CheckedInputStream, DataInputStream, DigestInputStream, InflaterInputStream, LineNumberInputStream und PushbackInputStream. Die Klasse LineNumberInputStream sollte nicht mehr verwendet werden, da sie veraltet ist.

Eine Filterklasse überschreibt alle Methoden von InputStream und OutputStream und ersetzt diese durch neue Methoden mit erweiterter Funktionalität. Für die oben genannten Klassen zeigt sich diese zusätzliche Funktionalität in folgenden Leistungen: Daten können gepuffert, mit einer Checksumme versehen, gepackt oder in den Lesestrom zurückgelegt werden. Zudem vereinfachen einige Klassen den Zugriff auf die Daten enorm.

Abbildung

Am UML-Diagramm fällt besonders auf, dass jeder Filter zum einen ein Stream ist, und zum anderen einen Stream verwaltet. Damit nimmt er Daten entgegen und leitet sie gleich weiter.


Galileo Computing

12.4.8 Der besondere Filter PrintStream  downtop

Schon in den ersten Programmen haben wir ein PrintStream-Objekt verwendet – doch vermutlich, ohne es zu wissen. Es steckte im out-Attribut der Klasse System. Es sind die vielen überladenen println()- und print()-Funktionen, die auf allen Datentypen operieren. So ist auch die Aufgabe definiert: Ausgabe unterschiedlicher Daten mittels einer Funktion. Im Gegensatz zum DataOutputStream erzeugt das PrintStream auch keine IOException. Intern setzt die Klasse jedoch ein Flag, welches durch die Methode checkError() nach außen kommt. Technisch gesehen ist ein PrintStream ein FilterOutputStream. So wie jeder Filter muss ein PrintStream-Objekt mit einem Ausgabeobjekt verbunden sein. Im Konstruktor kann zusätzlich noch ein Auto-flush übergeben werden, das bestimmt, ob die Daten bei einem println() oder beim Byte »\n« in einer Zeichenkette aus dem Puffer gespült werden. Alle Methoden sind synchronized und die Ausgabemethoden passen die Zeichenketten an die jeweilige Kodierung an.

Abbildung

class java.io.PrintStream
extends FilterOutputStream

gp  PrintStream( OutputStream out, boolean autoFlush )
Erzeugt einen neuen PrintStream, der automatisch beim Zeilenende den Puffer leert.
gp  PrintStream( OutputStream out )
Erzeugt einen neuen PrintStream.
gp  boolean checkError()
Testet, ob intern eine IOException aufgetreten ist.
gp  void close()
Schließt den Stream.
gp  void flush()
Schreibt den Puffer.
gp  void print( boolean|char|char[]|double|float|int|long|String )
Schreibt die primitiven Objekte.
gp  void print( Object o )
Ruft o.toString() auf und gibt das Ergebnis aus, wenn o ungleich null ist. Sonst ist die Ausgabe null.
gp  void println()
Schließt die Zeile mit einem Zeilenendezeichen ab.
gp  void println( boolean|char|char[]|double|float|int|long|String|Object )
Wie oben.
gp  protected void setError()
Setzt den Fehlerstatus auf true.
gp  void write( byte[] buf, int off, int len )
Schreibt len Bytes des Felds ab off in den Strom.
gp  void write( int b )
Schreibt Byte b in den Strom.

Galileo Computing

12.4.9 System Standard-Ein- und Ausgabe und Input- bzw. PrintStreams  downtop

Für die Standard-Eingabe- und Ausgabegeräte, die normalerweise Tastatur und Bildschirm sind, sind zwei besondere Stream-Klassen definiert, die beim Laden der Klasse automatisch erzeugt werden und von uns genutzt werden können. Dies ist zum einen das System.in-Objekt für die Eingabe, und zum anderen das Objekt System.out für die Ausgabe. System.in ist ein Exemplar der Klasse InputStream und System.out bzw. System.err ein Exemplar von PrintStream.

Beispiel Ein Programm, das eine Benutzereingabe einliest und anschließend auf den Bildschirm schreibt.

Listing 12.10   TestAusgabe.java
import java.io.*;

class TestAusgabe
{
public static void main( String args[] )
{
byte buffer[] = new byte[255];
System.out.println( "\nGib mir eine Zeile Text: " );
try
{
System.in.read( buffer, 0, 255 );
}
catch ( Exception e ) {
System.out.println( e );
}
System.out.println( "\nDu hast mir gegeben: ");

System.out.println( new String(buffer) );
}
}
final class java.lang.System

gp  InputStream in
Dies ist der Standard-Eingabestrom. Er ist immer geöffnet und nimmt die Benutzereingaben normalerweise über die Tastatur entgegen.
gp  void setIn( InputStream in )
Der Eingabestrom kann umgesetzt werden, um beispielsweise aus einer Datei oder Netzwerkverbindung Daten zu beziehen, die an in anliegen sollen.
gp  PrintStream out
Der Standard-Ausgabestrom. Er ist immer geöffnet und normalerweise mit der Bildschirmausgabe verbunden.
gp  void setOut( PrintStream out )
Der Standard-Ausgabe-Kanal wird umgesetzt.
gp  PrintStream err
Der Standard-Fehler-Ausgabestrom. Er wurde eingeführt, um die Fehlermeldungen von den Ausgabemeldungen zu unterscheiden. Auch wenn der Ausgabe-Kanal umgeleitet wird, bleiben diese Meldungen erreichbar.
gp  void setErr( PrintStream err )
Der Fehler-Kanal wird auf den PrintStream err gesetzt.

Andere Stromanbieter

Für Applikationen ist es nur möglich, über die oben genannten Methoden die Standardeingabe auf einen beliebigen InputStream und die Standardausgabe auf einen beliebigen PrintStream umzuleiten. Bei einem Applet bekommen wir eine Security-Exception, da keine Ausgaben unterdrückt werden dürfen. Zum Ändern dienen die Methoden setIn(), setOut() und setErr(). Das erstaunliche der System-Klasse ist jedoch, dass die Attribute in, out und err final sind und daher eigentlich nicht geändert werden können. Die Implementierung sieht daher auch etwas ungewöhnlich aus:

public final static InputStream 
in  = nullInputStream();
public final static PrintStream out = nullPrintStream();
public final static PrintStream err = nullPrintStream();

Die Methode nullPrintStream() ist noch skurriler.

private static InputStream nullInputStream()
throws NullPointerException {
if (currentTimeMillis() > 0)
return null;
throw new NullPointerException();
}

Da bleibt die Frage natürlich, wieso die Sun-Entwickler nicht gleich den Wert auf null gesetzt haben. Denn nichts anderes macht ja die intelligente Funktion. Die Lösung liegt im final-Konstruktor und in einer Compiler-Optimierung. Da finale Variablen später (eigentlich) nicht mehr verändert werden dürfen, kann der Compiler überall, wo in, out oder err vorkommt, eine null einsetzen und nicht mehr auf die Variablen zurückgreifen. Dies muss aber verboten werden, da diese drei Attribute später mit sinnvollen Referenzen belegt werden. Genauer gesagt ist dafür die Methode initializeSystemClass() zuständig. Ursprünglich kommen die Ströme aus dem FileDescriptor. Für err und out werden dann hübsche BufferedOutputStream mit 128 Bytes Puffer angelegt, die sofort durchschreiben. in ist ein einfacher BufferedInputStream mit der Standard-Puffergröße. Damit setIn(), setOut() und setErr() dann auf die final-Variable schreiben dürfen, müssen selbstverständlich native Methoden her, die setIn0(in), setOut0(out) und setErr0(err) heißen. Die Parameter der Funktionen sind die Parameter der Set-Methoden. In initializeSystemClass() werden dann auch auf den gepufferten Strömen diese nativen Methoden angewendet.

Die Bastelei mit der nullPrintStream()-Methode ist nicht nötig, wenn der Java-Standard 1.1 vorliegt. Denn dort hat sich eine Kleinigkeit getan, die erst zu der umständlichen Lösung führte. Warum musste denn der Compiler die final-Variablen auch vorbelegen? Die Antwort ist, dass der 1.0 konforme Compiler direkt die final-Variablen initialisieren musste. Erst seit 1.1 kann an einer anderen Stelle genau einmal auf final-Variablen schreibend zugegriffen werden. Wir haben das schon an anderer Stelle beschrieben und die fehlerhafte Jikes-Implementierung aufgeführt, die auch zu Anfang in Javac zu Problemen bei der Doppelbelegung führte. Legen wir einen Java 1.1-Compiler zu Grunde, was heute selbstverständlich ist, lässt sich die nullPrintStream() vermeiden. Wir können das an der Kaffe-Implementierung nachsehen, denn dort findet sich einfach:

final public static InputStream 
in;
final public static PrintStream out;
final public static PrintStream err;

Jetzt wird an einer definierten Stelle die Ein- bzw. Ausgabe initialisiert.

in = new BufferedInputStream(
new FileInputStream(FileDescriptor.in), 128);
out = new PrintStream( new BufferedOutputStream(
new FileOutputStream(FileDescriptor.out), 128), true);
err = new PrintStream(new BufferedOutputStream(
new FileOutputStream(FileDescriptor.err), 128), true);

Im Unterschied zu der Sun-Implementierung muss hier nicht schon ein Aufruf der nativen Methoden verwendet werden, obwohl dies spätestens bei den setXXX()-Methoden nötig wird. Die Einfachheit einer solchen nativen Routine wollen wir uns zum Schluss einmal an setOut0() anschauen:

void Java_java_lang_System_setOut0
(JNIEnv *env, jclass system, jobject stream)
{
jfieldID out = (*env)->GetStaticFieldID(
env, system,
"out", "Ljava/io/PrintStream;");
assert(out);
(*env)->SetStaticObjectField(env, system, out, stream);
}

Dies zeigt noch einmal sehr deutlich, dass final durch native Methoden außer Kraft gesetzt werden kann. Diese Lösung ist aber, wie wir schon festgestellt haben, sehr unschön. Damit aber die einfache Verwendung von out, err und in als Attribut möglich ist, bleibt außer dieser Konstruktion nicht viel übrig. Andernfalls hätte eine Methode Eingang finden müssen, aber

System.out().println( "Hui Buh" 
);

macht sich nicht so schön. Ließen sich in Java Variablen mit einem speziellen Modifizierer versehen, der nur den Lesezugriff von außen gewährt und nicht automatisch einen Schreibzugriff, so wäre das auch eine Lösung. Doch so etwas gibt es in Java nicht. Softwaredesigner würden sowieso Methoden nehmen, da sie Variablenzugriffe meist meiden.

Ausgabe in ein Fenster

Bei genauerer Betrachtung der Standard-Ausgabe- und Eingabe-Methoden ist festzustellen, dass das Konzept nicht besonders plattformunabhängig ist. Wenn wir einen Macintosh als Plattform betrachten, dann lässt sich dort keine Konsole ausmachen. Bei GUI-Anwendungen spricht demnach einiges dafür, auf die Konsolenausgabe zu verzichten und die Ausgabe in ein Fenster zu setzen. Ich möchte daher an dieser Stelle etwas vorgreifen und ein Programmstück vorstellen, mit dem sich die Standard-Ausgabeströme in einem Fenster darstellen lassen. Dann genügt Folgendes, unter der Annahme, dass die Variable ta ein TextArea-Objekt referenziert:

PrintStream p = new PrintStream() 
{
new OutputStream() {
public void write( int b ) {
ta.append ( ""+(char)b );
}
}
}
System.setErr( p );
System.setOut( p );

Den Bildschirm löschen und Textausgaben optisch aufwerten

Die Java-Umgebung setzt keine spezielle grafische Architektur voraus und kein spezielles Terminal. Als unabhängige Sprache gibt es daher außer der Textausgabe bisher keine Möglichkeit, die Farben für die Textzeichen zu ändern, den Cursor zu setzen oder den Bildschirm zu löschen. Bei Programmen mit Textausgabe sind dies aber gewünschte Eigenschaften. Wir können jedoch bei speziellen Terminals Kontrollzeichen ausgeben, sodass die Konsole Attribute speichert und Text somit formatiert ausgegeben werden kann. Bei einem VT100-Terminal existieren unterschiedliche Kontrollsequenzen und über eine einfache Ausgabe System.out.println() lässt sich der Bildschirm löschen.

System.out.println( "\u001b[H\u001b[2J" 
);

Leider ist diese Lösung nur lauffähig auf einem VT100-Terminal. Andere Varianten müssen speziell angepasst werden.

Schreibarbeit sparen

Natürlich ist es Schreibarbeit, immer die Angabe System.out.bla machen zu müssen, so wie in

System.out.println( "Das Programm 
gibt die Ausgabe: " );
System.out.println( 1.234 );
System.out.println( "Die drei Fragezeichen sind toll." );

Durch eine zusätzliche Referenz können wir uns Arbeit sparen. Das ganze funktioniert, da System.out ein Objekt vom Typ PrintStream ist.

Listing 12.11   SchnellerPrintStream.java
import java.io.*;

public class SchnellerPrintStream
{
private static final
PrintStream o = System.out;

public static void main( String args[] )
{
o.println( "Neu!" );
o.println( "Jetzt noch weniger zu schreiben." );
o.println( "Hilft auch Gelenken wieder auf die Sprünge!" );
}
}

In der Klasse OutputStream taucht die Funktion println() nicht auf. Für Systemausgaben wäre OutputStream auch viel zu unflexibel, daher leitet sich die Klasse (wie auch im Beispiel gezeigt) von PrintStream ab. Mittlerweile (also seit Java 1.1) sollte die Klasse nicht mehr verwendet werden und wenn doch, dann nur noch für Debugcode und aus Kompatibilitätsgründen. Wir stützen uns daher auf die Klasse PrintWriter, die die abstrakte Klasse Writer erweitert. So bleibt das Attribut System.out weiterhin vom Typ PrintStream. Die Deklaration PrintWriter o = System.out ist also falsch (das ist zum Beispiel solch ein Kompatibilitätsgrund).

Wenn selbst Math zu lang ist

Im Übrigen funktioniert diese Zuweisung auch bei anderen Klassen, zum Beispiel Math.

Listing 12.12   ShortRefMath.java
class ShortRefMath
{
Math m = null;

double d = m.sin( 1.34 );
}

Dies ist jedoch ein schmutziger Trick. Die Null-Referenz ist nötig, da andernfalls m nicht initialisiert wäre. Dies funktioniert nur deshalb, da alle Funktionen aus Math statisch sind.


Galileo Computing

12.4.10 Bytes in den Strom mit ByteArrayOutputStream  downtop

Die Klasse ByteArrayOutputStream lässt sich gut verwenden, wenn mehrere unterschiedliche primitive Datentypen in ein Byte-Feld kopiert werden sollen, die dann später eventuell binär weiterkodiert werden müssen. Erstellen wir etwa eine GIF-Datei , so müssen wir nacheinander verschiedene Angaben schreiben. So erstellen wir leicht eine Grafikdatei im Speicher. Anschließend konvertieren wir mit toByteArray() den Inhalt des Datenstroms in ein Byte-Feld.

ByteArrayOutputStream boas = new 
ByteArrayOutputStream();
DataOutputStream out = new DataOutputStream( boas );

// Header

out.write( 'G' ); out.write( 'I' ); out.write( 'F' );
out.write( '8' ); out.write( '9' ); out.write( 'a' );

// Logical Screen Descriptor

out.writeShort( 128 ); // Logical Screen Width (Unsigned)
out.writeShort( 37 ); // Logical Screen Height (Unsigned)

// <Packed Fields>, Background Color Index, Pixel Aspect Ratio,
// usw.

out.close();

byte result[] = out.toByteArray();

Galileo Computing

12.4.11 Ströme zusammensetzen mit SequenceInputStream  downtop

Ein SequenceInputStream-Filter hängt viele Eingabeströme zu einem großen Eingabestrom zusammen. Nützlich ist dies, wenn wir aus Strömen lesen wollen und es uns egal ist, was für ein Strom es ist, wo er startet und wo er aufhört.

Ein SequenceInputStream kann erzeugen, in dem im Konstruktor zwei InputStream-Objekte mitgegeben werden. Soll aus zwei Dateien ein zusammengesetzter Datenstrom gebildet werden, nutzen wir folgende Programmzeilen:

InputStream s1 = new FileInputStream( 
"teil1.txt" );
InputStream s2 = new FileInputStream( "teil2.txt" );
InputStream s =
new SequenceInputStream( s1, s2 );

Ein Aufruf irgendeiner read()-Methode liest nun erst Daten aus s1. Liefert s1 keine Daten mehr, kommen die Daten aus s2. Liegen keine Daten mehr an s2, aber wieder an s1, ist es zu spät. Natürlich hätten wir diese Funktionalität auch selber programmieren können, doch wenn etwa eine Methode nur einen InputStream verlangt, ist diese Klasse sehr hilfreich.

Für drei Ströme kann eine Kette aus zwei SequenceInputStream-Objekten gebaut werden.

InputStream in = new SequenceInputStream( 
stream1,
new SequenceInputStream(stream2, stream3) );

Sollen mehr als zwei Ströme miteinander verbunden werden, kann eine Enumeration im Konstruktor übergeben werden. Die Enumeration einer Datenstruktur gibt dann die zu kombinierenden Datenströme zurück. Wir haben eine Datenstruktur, die sich hier gut anbietet: Vector. Wir packen alle Eingabeströme in einen Vector und nutzen dann die elements()-Methode für die Aufzählung.

Vector v = new Vector();
v.addElement( stream1 );
v.addElement( stream2 );
v.addElement( stream3 );
InputStream seq = new SequenceInputStream( v.elements() );
class java.io.SequenceInputStream
extends InputStream

gp  SequenceInputStream( InputStream s1, InputStream s2 )
Erzeugt einen SequenceInputStream aus zwei einzelnen InputStream-Objekten. Zuerst werden die Daten aus s1 gelesen und dann aus s2.
gp  SequenceInputStream( Enumeration e )
Die Eingabeströme für den SequenceInputStream werden aus der Enumeration mit next Element() geholt. Ist ein Strom ausgesaugt, wird die close()-Methode aufgerufen und der nächste vorhandene Strom geöffnet.
gp  int available() throws IOException
Liefert die Anzahl der Zeichen, die gelesen werden können. Die Daten betreffen immer den aktuellen Strom.
gp  int read() throws IOException
Liefert das Zeichen oder -1, wenn das Ende aller Datenströme erreicht ist.
gp  int read( byte[] b, int off, int len ) throws IOException
Liest Zeichen in ein Feld und gibt die Anzahl tatsächlich gelesener Zeichen oder –1 zurück.
gp  void close() throws IOException
Schließt alle Ströme, die vom SequenceInputStream-Objekt eingebunden sind.

Implementierung durch einen internen Vector

Eine interessante Frage ist, ob SequenceInputStream auf zwei InputStream-Objekte irgendwie anderes behandelt wird, als die Variante mit dem Enumerator. Offensichtlich muss die Implementierung ja anders aussehen. Technisch gesehen gibt es aber für read() keinen Unterschied, denn SequenceInputStream mit nur zwei Strömen ist intern ein Vector, der temporär aufgebaut wird. Es kommt ja nur auf den Enumerator an. So ergibt sich folgender Programmcode:

public SequenceInputStream(InputStream 
s1, InputStream s2) {
Vector v = new Vector(2);
v.addElement(s1);
v.addElement(s2);
e = v.elements();
try {
nextStream();
} catch ( IOException ex ) {
// This should never happen
throw new Error("panic");
}
}

Das folgende Programm zeigt, wie ein StringBufferInputStream mit einem Datenstrom zusammengelegt wird. Es werden anschließend Zeilennummern und Zeileninhalt ausgeben, wobei sehr schön deutlich wird, das erst der String und dann die Datei ausgelesen wird. Die Datei muss sich im Pfad befinden, da sie sonst nicht gefunden werden kann.

Listing 12.13   SequenceInputStreamDemo.java
import java.io.*;

public class SequenceInputStreamDemo
{
public static void
main( String args[] ) throws FileNotFoundException
{
StringBufferInputStream sbis;
FileInputStream fis;

String s = "Der\nString\ninim\nStringBuffer\n";

sbis = new StringBufferInputStream( s );
fis = new FileInputStream("SequenceInputStreamDemo.java");

SequenceInputStream sis = new SequenceInputStream(sbis, fis);

LineNumberInputStream lsis = new LineNumberInputStream( sis );

int c;

System.out.print( "1: " );

try
{
while ( ( c = lsis.read())!= -1 )
{
if ( c == '\n' )
System.out.print( "\n" + lsis.getLineNumber() + ": " );
else
System.out.print( (char)c );
}
} catch ( IOException e ) { System.out.println( e ); }

System.out.println();
}
}

Leider verwendet das Programm zwei veraltete Klassen: StringBufferInputStream und LineNumberInputStream. Wir sind angehalten, für beide Klassen Reader-Klassen zu nutzen. Leider gibt es für Reader keinen Sequence-Strom und so haben die beiden veralteten Klassen zur Demonstration ihre Berechtigung.


Galileo Computing

12.5 Die Unterklassen von Writer  downtop

Alle Klassen, die Unicode-Zeichen hintereinander ausgeben, basieren auf der abstrakten Klasse Writer. Aus Writer leiten sich später Klassen ab, die konkrete Ausgabegeräte ansprechen oder Daten filtern. Folgende Tabelle zeigt die aus Writer abgeleiteten Klassen:

Tabelle 12.3   Übersicht der von Writer direkt abgeleiteten Klassen
Klasse Bedeutung
OutputStreamWriter Abstrakte Basisklasse für alle Writer, die einen Zeichen-Stream in einen Byte-Stream umwandeln
FilterWriter Abstrakte Basisklasse für Filterobjekte
PrintWriter Ausgabe der primitiven Datentypen
BufferedWriter Writer, der puffert
StringWriter Writer, der in einen String schreibt
CharArrayWriter Writer, der in ein Feld schreibt
PipedWriter Writer zur Ausgabe in einen passenden PipedReader

Wir lernen noch die Klasse FileWriter kennen, die nicht direkt aus Writer hervorgeht. Hier hängt noch eine weitere Klasse dazwischen, die Unterklasse von Writer ist: OutputStreamWriter.


Galileo Computing

12.5.1 Die abstrakte Klasse Writer  downtop

Basis für alle wichtigen Klassen ist die abstrakte Basisklasse Writer.

abstract class java.io.Writer

gp  protected Writer()
Erzeugt einen neuen Writer-Stream, der zudem ein Synchronisations-Objekt initialisiert.
gp  Writer( Object lock )
Erzeugt einen neuen Writer-Stream, der sich mit dem übergebenen Synchronisations-Objekt initialisiert. Ist die Referenz null, so gibt es eine NullPointerException.
gp  void write( int c ) throws IOException
Schreibt ein einzelnes Zeichen. Von der 16 Bit Ganzzahl wird der niedrige Teil geschrieben.
gp  void write( char[] cbuf ) throws IOException
Schreibt ein Feld von Zeichen.
gp  abstract void write( char[] cbuf, int off, int len ) throws IOException
Schreibt len Zeichen des Felds cbuf ab der Position off.
gp  void write( String str ) throws IOException
Schreibt einen String.
gp  void write( String str, int off, int len ) throws IOException
Schreibt len Zeichen der Zeichenkette str ab der Position off.
gp  abstract void flush() throws IOException
Schreibt den internen Puffer. Hängen verschiedene flush()-Aufrufe in einer Kette zusammen, die sich aus der Abhängigkeit der Objekte ergibt. So werden alle Puffer geschrieben.
gp  abstract void close() throws IOException
Schreibt den gepufferten Strom und schließt ihn. Nach dem Schließen durchgeführte write()- oder flush()-Aufrufe bringen eine IOException mit sich. Ein zusätzliches close() wirft keine Exception.

Wie die abstrakten Methoden genutzt und überschrieben werden

Uns fällt auf, dass von den sieben Methoden lediglich flush(), close() und write(char[], int, int) abstrakt sind. Zum einen bedeutet dies, dass konkrete Unterklassen nur diese Methoden implementierten müssen, und zum anderen, dass die übrigen write()-Funktionen auf die eine überschriebene Implementierung zurückgreifen. Werfen wir daher ein Blick auf die Nutznießer:

public void write(int c) throws 
IOException {
synchronized (lock) {
if (writeBuffer == null)
writeBuffer = new char[writeBufferSize];
writeBuffer[0] = (char) c;
write(writeBuffer, 0, 1);
}
}

Wird ein Zeichen geschrieben, so wird zunächst einmal nachgeschaut, ob schon jemand einen temporären Puffer eingerichtet hat. Wenn nicht, dann erzeugt die Funktion zunächst ein Array mit der Größe von 1024 Zeichen. (dies ist die eingestellte Puffer-Größe). Dann schreibt write(int) das Zeichen in den Puffer und ruft die abstrakte Methode auf. Ist der Parameter ein Feld, so muss lediglich die Größe an die abstrakte Methode übergeben werden. Alle Schreiboperationen sind mit einem lock-Objekt synchronisiert und können sich demnach nicht in die Quere kommen. Die Synchronisation wird entweder durch ein eigenes lock-Objekt durchgeführt, das dann im Konstruktor angegeben werden muss, oder die Klasse verwendet das this-Objekt der Writer-Klasse als Sperr-Objekt.

Schreiben einer Zeichenkette

Um einen Teil einer Zeichenkette zu schreiben, wird schon etwas mehr Aufwand betrieben. Ist der interne Puffer zu klein, wird ein neuer angelegt. Geschickt wird dieser Puffer cbuf gleich im Objekt gehalten, damit auch andere Funktionsaufrufe davon profitieren können. So wird vorgebeugt, dass vielleicht große Blöcke temporären Speichers verwendet werden.

public void write(String str, int 
off, int len)
throws IOException
{
synchronized (lock) {
char cbuf[];
if (len <= writeBufferSize) {
if (writeBuffer == null)
writeBuffer = new char[writeBufferSize];
cbuf = writeBuffer;
} else
cbuf = new char[len];
str.getChars(off, (off + len), cbuf, 0);
write(cbuf, 0, len);
}
}

Wir sehen bei der letzten übrig gebliebenen Funktion, dass sie write(String) in schlauer Weise nutzt.

public void write(String str) throws 
IOException {
write(str, 0, str.length());
}

Es liegt nun an den Unterklassen, diese Methoden zu überschreiben. Wir haben gesehen, dass lediglich die eine abstrakte Methode zwingend überschrieben werden muss, jedoch macht es durchaus Sinn, etwa zugunsten einer höheren Geschwindigkeit, auch die anderen Funktionen zu überschreiben.


Galileo Computing

12.5.2 Datenkonvertierung durch den OutputStreamWriter  downtop

Die Klasse OutputStreamWriter ist sehr interessant, da sie Konvertierungen der Zeichen nach einer Zeichenkodierung vornimmt. So wird sie für Ausgaben in Dateien, unterstützt durch die einzige Unterklasse FileWriter, noch wichtiger. Jeder OutputStreamWriter agiert dabei mit einem CharToByteConverter und konvertiert so Zeichenströme in Byte-Ströme. Die Zeichenkodierung kann im Konstruktor eines OutputStreamWriter-Objekts angegeben werden. Ohne Angabe ist es der Standard-Konvertierter, der in den Systemeigenschaften unter dem Schlüssel file.encoding geschrieben ist. Die Konvertierungs-Klassen liegen alle unter dem privaten Paket sun.io und sind für uns nicht sichtbar. Schauen wir hinter die Kulissen der Klasse. Aus dem String, der im Konstruktor übergeben wird, erzeugt eine statische Methode getConverter() der Klasse CharToByteConverter dann das CharToByteConverter-Objekt. Die interne Methode getDefault() liefert den Kodierer aus den System-Eigenschaften.

Die Kodierung der Zeichen ist in der Methode write(char [], int, int) angesiedelt. Jeder Aufruf dieser Funktion mündet in einen Konvertierungsaufruf der Methode convert Any(char cbuf[], int ci, int end, byte bb[], int nextByte, int nBytes) des CharToByteConverter-Objekts. Dies betrifft automatisch alle Funktionen, die auf write() basieren, etwa write(int) und weitere. Der erste Parameter ist das Original-Feld cbuf aus der write()-Methode. ci entspricht dem Offset, falls der Puffer noch nicht voll ist, und end ist die Summe von off und len. bb ist ein Puffer für die temporären Bytes, die später geschrieben werden. nextByte ist das nächste Byte, das in bb geschrieben wird und am Anfang 0 ist. Wenn der Puffer groß genug ist, bleibt nextByte auch 0. nByte ist die Größe des Puffer bb, die 8192 beträgt. Genaugenommen spielt auch nextByteIndex() eine Rolle, genau dann, wenn der interne Konvertierungspuffer überläuft. Doch das soll uns egal sein.

Wenn das Feld konvertiert worden ist, gibt es einen oder mehrere Aufrufe von write() auf dem Ausgabeobjekt mit dem internen Puffer bb.

Wenn wir von der statischen Methode getConverter(String) von CharToByteConverter sprechen, dann dürfen wir nicht vergessen, dass auch diese Funktion nicht in CharToByteConverter implementiert ist. Nutzen wir spezielle Konverter, so sind dies immer Unterklassen von CharToByteConverter, etwa CharToByteISO8859_1 für einen ISO8859_1 Konverter. Diese Methode muss nicht immer mitgeschleppt werden, sondern ist in der Klasse Converters verankert. getConverter() ruft nur die statische Methode newConverter() in Converters auf. Hier finden sich einige Abfragen, die ISO8859_1 und 8859_1 auf die gleiche Konverterklasse abbilden. Der Name der Klasse wird dabei aus dem Präfix »CharToByte« oder auch »ByteToChar« und dem Namen des Konverters zusammengesetzt.

class java.io.OutputStreamWriter
extends Writer

gp  OutputStreamWriter( OutputStream out )
Erzeugt einen OutputStreamWriter, der die Standard-Kodierung verwendet.
gp  OutputStreamWriter( OutputStream out, String enc )
Erzeugt einen OutputStreamWriter mit der vorgegebenen Kodierung.
gp  void close()
Schließt den Datenstrom.
gp  void flush()
Schreibt den gepufferten Strom.
gp  String getEncoding()
Liefert die Kodierung des Datenstroms als String.
gp  void write( char[] cbuf, int off, int len )
Schreibt Zeichen des Felds.
gp  void write( int c )
Schreibt ein einzelnes Zeichen.
gp  void write( String str, int off, int len )
Schreibt den Teil eines Strings.

OutputStreamWriter muss write() von Writer überschreiben, damit die Kodierung durchgeführt werden kann. Wir haben ja schon gesehen, dass die übrigen Methoden auf diesen zwei write()-Funktionen aufbauen.


Galileo Computing

12.5.3 In Dateien schreiben mit der Klasse FileWriter  downtop

OutputStreamWriter ist die Basisklasse für die konkrete Klasse FileWriter, einer Klasse, die die Ausgabe in eine Datei erlaubt. FileWriter muss keine Methoden überschreiben und so fügt die Klasse nur vier Konstruktoren hinzu, damit eine Datei geöffnet werden kann.

Nachfolgendes Programm erstellt die Datei fileWriter.txt und schreibt eine Textzeile hinein. Da der Konstruktor und die write()-Methode eine IOException in dem Fall werfen, wenn ein Öffnen nicht möglich ist, müssen wir einen try/catch-Block um die Anweisungen setzen.

Listing 12.14   FileWriterDemo.java
import java.io.*;

public class FileWriterDemo
{
public static void main( String args[] )
{
try
{
FileWriter fw =
new FileWriter( "fileWriter.txt" );
fw.write( "Hallo Welt geht in eine Datei" );
fw.close();
} catch ( IOException e ) {
System.out.println( "Konnte Datei nicht erstellen" );
}
}
}

Hinter diesen Konstruktoren verbirgt sich ein FileOutputStream-Objekt. So konvertieren die write()-Methoden die Zeichenströme, aber letztendlich schreibt FileOutputStream die Daten.

public class FileWriter extends 
OutputStreamWriter
{
public FileWriter(String fileName) throws IOException {
super(new FileOutputStream(fileName));
}
public FileWriter(String fileName, boolean append)
throws IOException {
super(new FileOutputStream(fileName, append));
}
public FileWriter(File file) throws IOException {
super(new FileOutputStream(file));
}
public FileWriter(FileDescriptor fd) {
super(new FileOutputStream(fd));
}
}

Mit diesen vier Konstruktoren kann nun eine Datei geöffnet werden. Der einfachste Weg geht über den Dateinamen. Existiert die Datei schon, deren Name wir übergeben, so wird die Datei gelöscht. Um die Daten hinten anzuhängen, sollten wir als zweiten Parameter true angegeben. Eine zweite Möglichkeit, Daten hinten anzuhängen, ist die über die Klasse RandomAccessFile.


Galileo Computing

12.5.4 StringWriter und CharArrayWriter  downtop

Zwei interessante Klassen sind StringWriter und CharArrayWriter. Sie sind ebenfalls von Writer abgeleitet, schreiben jedoch die Ausgabe nicht in eine Datei, sondern in einen StringBuffer bzw. in ein Zeichen-Array. Die Felder werden automatisch vergrößert.

StringWriter

In den folgenden Programmzeilen konvertieren wir den StringWriter noch zu einem PrintWriter, damit wir die komfortable println()-Methode verwenden können.

StringWriter buffer = new 
StringWriter();
PrintWriter out = new PrintWriter( buffer );
out.println( "Ulli ist lieb" );
out.println( "Quatsch" );
String result = buffer.toString();

Hier findet der parameterlose Konstruktor Verwendung. Er legt einen StringWriter mit der Größe 16 an. (Standardgröße eines StringBuffer-Objekts). Daneben existiert aber noch ein Konstruktor mit dem Parameter int. Dieser legt dann die anfängliche Größe fest. Wenn unser StringWriter größer als 16 wird, und das ist wahrscheinlich, sollte immer der zweite Konstruktor Verwendung finden. So muss sich das interne StringBuffer-Objekt bei wiederholten write()-Aufrufen nicht immer in der Größe ändern. Mit den Funktionen getBuffer() und toString() lesen wir den Inhalt wieder aus. Die Methoden unterschieden sich darin, dass getBuffer() ein StringBuffer-Objekt zurückgibt und toString() das gewohnte String-Objekt.

class java.io.StringWriter
extends Writer

gp  StringWriter()
Erzeugt einen StringWriter mit der Startgröße 16.
gp  StringWriter( int initialSize )
Erzeugt einen StringWriter mit der angegeben Größe.
gp  void close()
Schließt den StringWriter.
gp  void flush()
Schreibt die gepufferten Daten in den Stream.
gp  StringBuffer getBuffer()
Liefert den StringBuffer.
gp  String toString()
Liefert den Puffer als String.
gp  void write( char[] cbuf, int off, int len )
Schreibt einen Teil der Zeichen des Felds.
gp  void write( int c )
Schreibt ein einzelnes Zeichen.
gp  void write( String str )
Schreibt einen String.
gp  void write( String str, int off, int len )
Schreibt den Teil eines Strings.

Die Methode close()

Der interne StringBuffer wird nicht freigegeben. Außerdem lassen sich nach dem Schließen trotzdem noch Zeichen schreiben. Ein Beispiel: Die Schreib-Funktionen erwarten, dass der Stream offen ist. Dazu wird immer die private Funktion ensureOpen() aufgerufen. Etwa bei folgender write()-Methode:

public void write(int c) {
ensureOpen();
buf.append((char) c);
}

Nun werfen wir ein Blick auf die Methode ensureOpen() und auf den Kommentar.

private void ensureOpen() {
/* This method does nothing for now. Once we add throws clauses
* to the I/O methods in this class, it will throw an IOException
* if the stream has been closed. */
}

CharArrayWriter

Neben StringWriter schreibt auch die Klasse CharArrayWriter Zeichen in einen Puffer, jedoch diesmal in ein Zeichen-Feld. Sie bietet zudem drei zusätzliche Funktionen an: reset(), size() und writeTo().

class java.io.CharArrayWriter
extends Writer

gp  CharArrayWriter()
Erzeugt einen neuen CharArrayWriter.
gp  CharArrayWriter( int initialSize )
Erzeugt einen neuen CharArrayWriter mit einer Standardgröße.
gp  void close()
Schließt den Stream.
gp  void flush()
Leert den Stream.
gp  void reset()
Setzt den internen Puffer zurück, sodass das CharArrayWriter-Objekt ohne neue Speicheranforderung genutzt werden kann.
gp  int size()
Liefert die Größe des Puffers.
gp  char[] toCharArray()
Gibt eine Kopie der Eingabedaten zurück. Es ist wirklich eine Kopie und keine Referenz.
gp  String toString()
Konvertiert die Eingabedaten in einen String.
gp  void write( char[] c, int off, int len )
Schreibt Zeichen in den Puffer.
gp  void write( int c )
Schreibt ein Zeichen in den Puffer.
gp  void write( String str, int off, int len )
Schreibt einen Teil eines String in den Puffer.
gp  void writeTo( Writer out )
Schreibt den Inhalt des Puffers in einen anderen Zeichenstrom. Diese Methode ist ganz nützlich, um die Daten weiterzugeben.
gp  Writer als Filter verketten
Die Funktionalität der bisher vorgestellten Writer-Klassen reicht für den Alltag zwar aus, doch sind Ergänzungen gefordert, die den Nutzen oder die Fähigkeiten der Klassen erweitern. In Java gibt es die drei Klassen BufferedWriter, PrintWriter und FilterWriter, die einen Writer im Konstruktor erlauben und ihre Ausgabe an diesen weiterleiten. Die neue Klasse erweitert die Funktionalität, schreibt ihre Ausgabe in den alten Writer und die Klassen werden somit geschachtelt. Es lassen sich auch alle drei Klassen allesamt ineinander verschachteln.

Hier bauen wir zunächst einen FileWriter. Dieser sichert die Daten, die mittels write() gesendet werden, in einer Datei. Anschließend erzeugen wir einen BufferedWriter, der die Daten, die in die Datei geschrieben werden, zunächst sammelt. Diesen BufferedWriter erweitern wir noch zu einem PrintWriter, da ein PrintWriter neue Schreib-Funktionen besitzt, sodass wir nicht mehr nur auf write()-Methoden angewiesen sind, sondern die komfortablen print()-Funktionen nutzen können.

Beispiel Writer mit Filter verkettet Listing 12.15   CharArrayWriterDemo.java
import java.io.*;

public class CharArrayWriterDemo
{
public static void main( String args[] )
{

try
{
FileWriter fw =
new FileWriter("charArrayWriterDemoPuffer.txt");
BufferedWriter bw =
new BufferedWriter( fw );
PrintWriter pw = new PrintWriter( bw );

for ( int i = 1; i < 10000; i++ )
pw.println( "Zeile " + i );

pw.close();

} catch ( IOException e ) {
System.out.println( "Konnte Datei nicht erstellen" );
}
}
}


Galileo Computing

12.5.5 Gepufferte Ausgabe durch BufferedWriter  downtop

Die Klasse BufferedWriter hat die Aufgabe, Dateiausgaben, die mittels write() in den Stream geleitet werden, zu puffern. Dies ist immer dann nützlich, wenn viele Schreiboperationen gemacht werden, denn das Puffern macht die Dateioperationen wesentlich schneller, da so mehrere Schreiboperationen zu einer zusammengefasst werden. Um die Funktionalität eines Puffers zu erhalten, enthält ein BufferedWriter-Objekt einen internen Puffer, in dem die Ausgaben von write() zwischengespeichert werden. Standardmäßig ist dieser Puffer 8192 Zeichen groß. Er kann aber über einen parametrisierten Konstruktor auf einen anderen Wert gesetzt werden. Erst wenn der Puffer voll ist oder die Methoden flush() oder close() aufgerufen werden, werden die gepufferten Ausgaben geschrieben. Durch die Verringerung tatsächlicher write()-Aufrufe an das externe Gerät wird die Geschwindigkeit der Anwendung deutlich erhöht.

Abbildung

Um einen BufferedWriter anzulegen, existieren zwei Konstruktoren, denen ein bereits existierender Writer übergeben wird. Nach dem flush(), close() oder internen Überlauf wird die Ausgabe an den übergeben Writer weitergereicht.

Zusätzlich bietet die Klasse die Methode writeLine(), die in der Ausgabe eine neue Zeile beginnt. Das Zeichen für den Zeilenwechsel wird aus der Systemeigenschaft line.separator genommen. Da sie intern mit der write()-Methode arbeitet, kann sie eine IOException auslösen.

Das folgende Programm kopiert den Inhalt einer Textdatei und erzeugt dabei eine neue Datei:

Listing 12.16   ReadWriter.java
import java.io.*;

public class ReadWriter
{
public static void main( String args[] )
{
if ( args.length != 2 ) {
System.err.println( "usage: infile outfile\n" );
System.exit( 1 );
}

try
{
BufferedReader in = new BufferedReader(
new FileReader( args[0] ) );

BufferedWriter out = new BufferedWriter(
new FileWriter( args[1] ) );

String s = null;

while ( (s = in.readLine()) != null )
{
out.write( s );
out.newLine();
}

in.close();
out.close();

} catch ( IOException e ) {
System.err.println( e );
}
}
}
Tipp Ein Geschwindigkeitstipp noch zum Schluss. Falls bekannt, sollte die Puffergröße des BufferedWriter (gleiches gilt für BufferedReader) mit der internen Puffergröße des Betriebssystems übereinstimmen. Hier können Geschwindigkeitsmessungen mit unterschiedlichen Puffergrößen die Lösung bringen.

class java.io.BufferedWriter
extends Writer

gp  BufferedWriter( Writer out )
Erzeugt einen puffernden BufferedWriter mit der Puffergröße 8 k.
gp  BufferedWriter( Writer out, int sz )
Erzeugt einen puffernden BufferedWriter mit der Puffergröße sz, die nicht kleiner 0 sein darf. Andernfalls gibt es eine IllegalArgumentException. Die Puffergröße 0 ist erlaubt, aber unsinnig.
gp  void write( int c ) throws IOException
Schreibt ein einzelnes Zeichen.
gp  void write( char[] cbuf, int off, int len ) throws IOException
Schreibt einen Teil des Zeichenfelds.
gp  void write( String s, int off, int len ) throws IOException
Schreibt einen Teil des Strings.
gp  void newLine() throws IOException
Schreibt einen Zeilenvorschub, der in den Systemeigenschaften festgelegt ist.
gp  void flush() throws IOException
Leert den Stream.
gp  void close() throws IOException
Schließt den Strom. Kommen Anweisungen danach, wird eine IOException ausgelöst.

Ein nettes Detail am Rande bietet die Implementierung von BufferedWriter. Wir finden hier die Deklaration der Methoden min(), die in den beiden write()-Methoden für Teilstrings bzw. Teilfelder Verwendung findet.

/**
Our own little min method, to avoid loading java.lang.Math if we've run
out of file descriptors and we're trying to print a stack trace.
*/
private int min(int a, int b) {
if (a < b) return a;
return b;
}

Galileo Computing

12.5.6 Ausgabemöglichkeiten durch PrintWriter erweitern  downtop

Bisher waren die Ausgabemöglichkeiten eines Writer-Objekts nur beschränkt. Die Möglichkeit mit write() Zeichen auszugeben ist für den Alltag zu wenig. Erweitert wird dieses Objekt durch die Klasse PrintWriter. Sie bietet Methoden für alle primitiven Datentypen und für den Objekttyp. Dafür bietet der PrintWriter eine Reihe überladener Methoden mit dem Namen print() an. Damit auch die Ausgabe mit einem zusätzlichen Zeilenvorschub nicht von Hand umgesetzt werden muss, gibt es für alle print()-Methoden eine entsprechende Variante println(), bei der automatisch am Ende der Ausgabe ein Zeilenumbruch angehängt wird. println() existiert auch parameterlos, um nur einen Zeilenumbruch zu setzen. Das Zeilenvorschubzeichen ist wie immer an die Plattform angepasst. Die Ausgabe in einen PrintWriter ist solange gepuffert, bis println() – beim entsprechenden Konstruktor angegeben – oder ein flush() ausgeführt wird.

class java.io.PrintWriter
extends Writer

gp  checkError()
Schreibt die gepufferten Daten und prüft Fehler. Die Abfrage ist wichtig, da die Klasse keine Eingabe- und Ausgabe-Exceptions wirft.
gp  void close()
Schließt den Strom.
gp  void flush()
Schreibt gepufferte Daten.
gp  void print( boolean|char|char[]|double|float|int|Object|String )
Schreibt Boolean oder Zeichen, Array von Zeichen, Double, Float, Integer, Object oder String.
gp  void println()
Schreibt Zeilenvorschubzeichen.
gp  void println( boolean|char|char[]|double|float|int|Object|String )
Schreibt Boolean oder Zeichen, Array von Zeichen, Double, Float, Integer, Objekt oder String und schließt die Zeile mit Zeilenendezeichen ab.
gp  void setError()
Zeigt an, dass ein Fehler auftrat.
gp  void write( char[] | int | String )
Schreibt Array von Zeichen, Integer oder String.
gp  write( char[], int off, int len )
Schreibt einen Teil (len Zeichen) eines Arrays beginnend bei off.
gp  write( String, int off, int len )
Schreibt einen Teilstring mit len Zeichen ab der Position off.

Keine der Methoden wirft eine IOException. Daher musste auch die Methode write(String) neu definiert werden, da die Funktion write() der Writer-Klasse eine IOException wirft. Intern wird eine mögliche Exception gefangen und ein internes Flag trouble gesetzt, das aber im Programm keine weiteren Einflüsse besitzt.

gp  PrintWriter( OutputStream out )
Erzeugt einen neuen PrintWriter aus einem OutputStream, der nicht automatisch am Zeilenende den Puffer schreibt.
gp  PrintWriter( OutputStream out, boolean autoFlush )
Erzeugt einen neuen PrintWriter aus einem OutputStream, der automatisch bei autoFlush gleich true am Zeilenende mittels println() den Puffer schreibt.
gp  PrintWriter( Writer out )
Erzeugt einen neuen PrintWriter aus einem Writer, der nicht automatisch am Zeilenende den Puffer schreibt.
gp  PrintWriter( Writer out, boolean autoFlush )
Erzeugt einen neuen PrintWriter aus einem OutputStream, der automatisch am Zeilenende mittels println() den Puffer leert, falls autoFlush=true ist.

Wir sehen, dass die Konstruktoren entweder mit einem OutputStream oder einem Writer arbeiten. Jeder Konstruktor existiert in zwei Varianten. Es gibt zusätzlich den Wahrheits-Parameter autoflush, der, wenn er true ist, angibt, ob nach einem Zeilenumbruch automatisch flush() aufgerufen wird.


Galileo Computing

12.5.7 Daten mit FilterWriter filtern  downtop

Die Architektur der Java-Klassen macht es leicht, auch eigene Filter zu programmieren. Basis ist dazu die abstrakte Klasse FilterWriter. Wir übergeben im Konstruktor ein Writer-Objekt, an das die Ausgaben weitergeleitet werden. Der Parameter wird in der Klasse in dem protected-Attribut out gesichert. In der Unterklasse greifen wir darauf zurück, denn dort schickt der Filter seine Ausgaben hin.

Ein FilterWriter überschreibt drei der write()-Methoden, sodass die Ausgaben an den Writer gehen.

abstract class java.io.FilterWriter
extends Writer

gp  protected Writer out
Der darunter liegende Ausgabestrom.
gp  protected FilterWriter( Writer out )
Erzeugt einen neuen filternden Writer.
gp  void close()
Schließt den Stream.
gp  void flush()
Leert den internen Puffer des Streams.
gp  void write( int c )
Schreibt ein einzelnes Zeichen.
gp  void write( char[] cbuf, int off, int len )
Schreibt einen Teil eines Zeichenfelds.
gp  void write( String str, int off, int len )
Schreibt einen Teil eines Strings.

Der Konstruktor der Klasse FilterWriter ist protected, denn ein FilterWriter sollte nicht erzeugt werden dürfen; die Klasse ist nur zum Erweitern da. Das ist aber nicht weiter schlimm. Für ableitende Klassen heißt dies, sie können einen Konstruktor mit dem Parameter vom Typ Write schreiben, und dann im Rumpf mit super(write) den Konstruktor von FilterWriter aufrufen.

Der Weg zum eigenen Filter

Damit nun unser eigener FilterWriter zum Leben erweckt werden kann, müssen wir nur drei Dinge beachten:

gp  Unsere Klasse leitet sich von FilterWriter ab.
gp  Unser Konstruktor bekommt als Parameter ein Writer-Objekt und ruft mit super(out) den Konstruktor der Oberklasse auf, was unser FilterWriter ist.
gp  Wir überlagern die drei write()-Methoden und eventuell noch die close()-Methode. Unsere write()-Methoden führen dann die Filterfunktionen aus und geben die wahren Daten an den Writer weiter.
Beispiel Die nachfolgende Klasse wandelt Zeichen des Stroms in Kleinbuchstaben um. Sie arbeitet mit Hilfsfunktionen der Character-Klasse.

Abbildung

Listing 12.17   LowerCaseWriterDemo.java
import java.io.*;

class LowerCaseWriter
extends FilterWriter
{
public LowerCaseWriter( Writer writer )
{
super( writer );
}

public void write( int c ) throws IOException
{
out.write( Character.toLowerCase((char)c) );
}

public void
write( char cbuf[], int off, int len ) throws IOException
{
out.write( String.valueOf(cbuf).toLowerCase(), off, len );
}

public void write( String s, int off, int len )
throws IOException
{
out.write( s.toLowerCase(), off, len );
}
}

public class LowerCaseWriterDemo
{
public static void main( String args[] )
{
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter( new LowerCaseWriter( sw ) );

pw.println( "Eine Zeile für klein und groß" );

System.out.println( sw.toString() );
}
}

Ein HTML-Writer

Unsere nächste Klasse bringt uns etwas näher an das HTML-Format heran. HTML steht für HyperText Markup Language. Wir wollen eine Klasse HTMLWriter entwerfen, die Filter Writer erweitert und Textausgaben in HTML konvertiert. In HTML werden Tags eingeführt, die vom Browser erkannt und besonders behandelt werden. Findet etwa der Browser im HTML-Text eine Zeile der Form <b>Dick</b>, so stellt er den Inhalt »Dick« in fetter Schrift da, da das <B>-Tag den Zeichensatz umstellt. Alle Tags werden in spitzen Klammern geschrieben. Daraus ergibt sich, dass HTML einige spezielle Zeichen verwendet. Wenn diese Zeichen auf der HTML-Seite darstellt werden, müssen sie durch eine spezielle Zeichensequenz dargestellt werden.

Tabelle 12.4   HTML-Zeichen mit alternativen Zeichensequenzen
Zeichen Zeichensequenz
< &lt;
> &gt;
& &amp;

Kommen diese Zeichen im Quelltext vor, so muss unser HTMLWriter diese Zeichen durch die entsprechende Sequenz ersetzen. Andere Zeichen sollen nicht ersetzt werden.

Den Browsern ist die Struktur der Zeilen in einer HTML-Datei egal. Sie formatieren wiederum nach speziellen Tags. Absätze etwa werden mit <p> eingeleitet, einfache Zeilenvorschübe mit <br>. Unser HTMLWriter soll zwei leere Zeilen durch einen Absatz-Tag markieren. Demnach sollte unser Programm Leerzeilen zählen.

Alle sauberen HTML-Dateien haben einen wohldefinierten Anfang und ein wohldefiniertes Ende. Folgendes kleine HTML-Dokument ist wohlgeformt und zeigt, was unser Programm für Einträge machen muss:

<HTML>
<HEAD><TITLE>
Title of page
</TITLE></HEAD>
<BODY>
</BODY>
</HTML>

Der Titel der Seite sollte im Konstruktor übergeben werden können.

Hier nun das Programm für den HTMLWriter:

Listing 12.18   HTMLWriter.java
import java.io.*;

class HTMLWriter extends FilterWriter
{
/**
* New constructor with title of the page.
*/
public HTMLWriter( Writer writer, String title )
{
super( writer );
try {
out.write( "<HTML><HEAD><TITLE>"
+ title
+ "</TITLE></HEAD><BODY>\n" );
} catch ( IOException e ) { }
}

/**
* Close the stream.
*/
public void close() throws IOException
{
try {
out.write( "</BODY></HTML>\n" );
} catch ( IOException e ) { }

out.close();
}

/**
* Needed constructor without title on the page.
*/
public HTMLWriter( Writer writer )
{
this( writer, "" );
}

/**
* Write a single character.
*/
public void write( int c ) throws IOException
{
switch ( c )
{
case '<' : out.write( "&lt;" ); newLine=false; break;
case '>' : out.write( "&gt;" ); newLine=false; break;
case '&' : out.write( "&amp;" ); newLine=false; break;
case '\n': if ( newLine ) {
out.write( "<P>\n" ); newLine=false;
}
else
out.write( "\n" );
newLine=true;
break;
case '\r': break; // ignore

default : out.write( (int) c ); newLine=false;
}
}

/**
* Write a portion of an array of characters.
*/
public void
write( char cbuf[], int off, int len ) throws IOException
{
for ( int i=off; i<len; i++ )
write( cbuf[i] );
}

/**
* Write a portion of a string.
*/
public void
write( String s, int off, int len ) throws IOException
{
for ( int i=off; i<len; i++ )
write( s.charAt( i ) );
}

private int lineNumberCnt;

private boolean newLine;
}

public class HTMLWriterDemo
{
public static void main( String args[] )
{
StringWriter sw = new StringWriter();

HTMLWriter html = new HTMLWriter( sw, "Toll" );

PrintWriter pw = new PrintWriter( html );

pw.println( "Und eine Menge von Sonderzeichen: <, > und &" );
pw.println( "Zweite Zeile" );
pw.println( );
pw.println( "Leerzeile" );
pw.println( "Keine Leerzeile danach" );

pw.close();

System.out.println( sw.toString() );

}
}

Im Demoprogramm erzeugen wir wieder einen StringWriter, in dem wir die Daten ablegen. Daher muss auch close() vor dem sw.toString() erfolgen, da wir andernfalls nicht den korrekten Abschluss sehen würden. Wenn wir nicht über den PrintWriter schließen würden, sondern über den HTMLWriter, dann müssten wir noch einen try/catch-Block um close() setzen, da sie eine IOException erzeugt. Nutzen wir aber PrintWriter, dann kümmert sich dieser darum, diese Exception zu fangen.

Unsere write()-Methoden sind sehr einfach, denn sie rufen für jedes Zeichen write(int) auf. Um das Programm hinreichend schnell zu machen, sollte also noch ein BufferedWriter um die Ausgaben gesetzt werden.

Die Ausgabe, die unser Programm nun erzeugt, ist Folgende:

<HTML><HEAD><TITLE>Toll</TITLE></HEAD><BODY>
Und eine Menge von Sonderzeichen: &lt; und &gt; und &amp;
Zweite Zeile
<P>
Leerzeile
Keine Leerzeile danach
</BODY></HTML>
Die Klassen um Reader

Die Basisklasse aller Eingaben ist die abstrakte Klasse Reader. Daraus leiten sich weitere Klassen ab, die sequenziellen Zugriff auf Daten erlauben. Wie bei den Writer-Objekten auch, beziehen sich die Unterklassen auf bestimmte Eingabegeräte mit bestimmtem Verhalten.

Die folgende Tabelle gibt einen Überblick auf die Klassen und einen Vorgeschmack, was in den folgenden Unterkapiteln beschrieben wird.

Mit dem FileReader lässt sich aus Dateien lesen. Die Klasse FileReader geht wie ein FileWriter nicht direkt aus der Klasse Reader hervor, sondern ein InputStreamReader sitzt hier noch dazwischen. Auch die Klasse LineNumberReader geht nicht direkt aus dem Reader hervor. Es ist eine Ableitung aus BufferedReader mit der Fähigkeit, Zeilen zu zählen.


Galileo Computing

12.5.8 Die abstrakte Basisklasse Reader  downtop

Die abstrakte Klasse Reader dient zum Lesen von Zeichen aus einem Eingabestrom. Die einzigen Methoden, die Unterklassen implementieren müssen, sind read(char[],int,int) und close(). Dies entspricht dem Vorgehen bei den Writer-Klassen, die auch nur close() und write(char[],int,int) implementieren müssen. Eine abstrakte flush()-Methode wie sie Writer besitzt, kann Reader nicht haben. Es bleiben demnach für die Reader-Klasse zwei abstrakte Methoden übrig. Die Unterklassen implementieren jedoch viele anderen Methoden aus Geschwindigkeitsgründen neu.

Tabelle 12.5   Von Reader direkt abgeleitete Klassen
Klasse Bedeutung
InputStreamReader Abstrakte Basisklasse für alle Reader, die einen Byte-Stream in einen Zeichen-Stream umwandeln
FilterReader Abstrakte Basisklasse für Eingabefilter
PushbackReader Eingabefilter, der Zeichen zurückgeben kann
BufferedReader Reader, der Zeilen puffert
StringReader Reader, der aus einem String liest
CharArrayReader Reader, der Zeichen-Arrays liest
PipedReader Reader, der aus einem PipedWriter liest

abstract class java.io.Reader

gp  protected Reader()
Erzeugt einen neuen Reader, der sich mit sich selbst synchronisiert.
gp  protected Reader( Object lock )
Erzeugt einen neuen Reader, der mit dem Objekt lock synchronisiert ist.
gp  abstract int read( char[] cbuf, int off, int len )
throws IOException
Liest len Zeichen in den Puffer cbuf ab der Stelle off. Wenn len Zeichen nicht vorhanden sind, wartet der Reader. Gibt die Anzahl gelesener Zeichen zurück oder –1, wenn das Ende des Stroms erreicht wurde.
gp  int read() throws IOException
Die parameterlose Methode liest das nächste Zeichen aus dem Eingabestrom. Die Methode wartet, wenn kein Zeichen im Strom bereit liegt. Der Rückgabewert ist ein int im Bereich 0 bis 16383 (0x00-0x3fff). Warum dann der Rückgabewert aber int und nicht char ist, kann leicht damit erklärt werden, dass die Methode den Rückgabewert –1 kodieren muss, falls keine Daten anliegen.
gp  int read( char[] cbuf ) throws IOException
Liest Zeichen in ein Feld. Die Methode wartet, bis Eingaben anliegen. Der Rückgabewert ist die Anzahl der gelesenen Zeichen oder –1, wenn das Ende des Datenstroms erreicht wurde.
gp  abstract void close() throws IOException
Schließt den Strom. Folgt anschießend noch ein read()-, ready()-, mark()- oder reset()-Aufruf, lösen diese eine IOException aus. Ein doppelt geschlossener Stream hat keinen weiteren Effekt.

Neben diesen notwendigen Methoden, die bei der Klasse Reader gegeben sind, kommen noch weitere interessante Funktionen hinzu, die den Status abfragen und Positionen setzen lassen. Die Methode ready() liefert als Rückgabe true, wenn ein read() ohne Blockierung der Eingabe möglich ist. Die Implementierung von Reader gibt immer false zurück. Mit der Methode mark() lässt sich eine bestimmte Position innerhalb des Eingabe-Stroms markieren. Die Methode sichert dabei die Position. Mit beliebigen reset()-Aufrufen lässt sich diese konkrete Stelle zu einem späteren Zeitpunkt wieder anspringen. mark() besitzt einen Ganzzahl-Parameter, der angibt, wie viele Zeichen gelesen werden dürfen, bevor die Markierung nicht mehr gültig ist. Die Zahl ist wichtig, da sie die interne Größe des Puffers bezeichnet, der für den Strom angelegt werden muss. Nicht jeder Datenstrom unterstützt dieses Hin- und Herspringen. Die Klasse StringReader unterstützt etwa die Markierung einer Position, die Klasse FileReader dagegen nicht. Daher sollte vorher mit markSupported() überprüft werden, ob das Markieren auch unterstützt wird. Wenn der Datenstrom es nicht unterstützt, und wir diese Warnung ignorieren, werden wir eine IOException bekommen. Denn Reader implementiert mark() und read() ganz einfach und muss von uns im Bedarfsfall überschrieben werden.

public void mark(int readAheadLimit) 
throws IOException {
throw new IOException("mark() not supported");
}
public void reset() throws IOException {
throw new IOException("reset() not supported");
}

Daher gibt markSupported() auch in der Reader-Klasse false zurück.

gp  long skip( long n ) throws IOException
Überspringt n Zeichen. Blockt, bis Zeichen vorhanden sind. Gibt die Anzahl der wirklich übersprungenen Zeichen zurück.
gp  public boolean ready() throws IOException
true
, wenn aus dem Stream direkt gelesen werden kann. Das heißt allerdings nicht, dass false immer Blocken bedeutet.
gp  boolean markSupported()
Der Stream unterstützt die mark()-Operation.
gp  void mark( int readAheadLimit ) throws IOException
Markiert eine Position im Stream. Der Parameter gibt an, nach wie vielen Zeichen die Markierung ungültig wird.
gp  void reset() throws IOException
Falls eine Markierung existiert, setzt der Stream an der Markierung. Wurde die Position vorher nicht gesetzt, dann wird eine IOException mit dem String »Stream not marked« geworfen.

Galileo Computing

12.5.9 Automatische Konvertierungen mit dem InputStreamReader  downtop

Unsere Basisklasse für alle Reader, die eine Konvertierung zwischen Byte- und Zeichen-Streams vornehmen, ist InputStreamReader. Die Klasse ist nicht abstrakt und hat demnach auch keine abstrakten Methoden. Sie arbeitet wie ein OutputStreamWriter und konvertiert die Daten mithilfe eines ByteToCharConverter. Da wir die Arbeitsweise schon an anderer Stelle beschrieben haben, verzichten wir nun darauf.

class java.io.InputStreamReader
extends Reader

gp  InputStreamReader( InputStream in )
Erzeugt einen InputStreamReader mit der Standard-Kodierung.
gp  InputStreamReader( InputStream in, String enc) throws UnsupportedEncodingException
Erzeugt einen InputStreamReader, der die angegebene Zeichenkodierung anwendet.
gp  String getEncoding()
Liefert einen String mit dem Namen der Kodierung zurück. Der Name ist kanonisch und kann sich daher von dem String, der im Konstruktor übergeben wurde, unterscheiden.
gp  int read() throws IOException
Liest ein einzelnes Zeichen oder gibt –1 zurück, falls der Stream am Ende ist.
gp  int read( char[] cbuf, int off, int len ) throws IOException
Liest einen Teil eines Felds.
gp  boolean ready() throws IOException
Kann vom Stream gelesen werden. Ein InputStreamReader ist bereit, wenn der Eingabepuffer nicht leer ist oder Bytes des darunter liegenden InputStreams anliegen.

Wie wir an dieser Stelle bemerken, unterstützt ein reiner InputStream kein mark() und reset(). Da FileReader die einzige Klasse in der Java-Bibliothek ist, die einen InputStreamReader erweitert, und diese Klasse ebenfalls kein mark() bzw. reset() unterstützt, lässt sich sagen, dass kein InputStreamReader der Standardbibliothek Positionsmarkierungen erlaubt.

Ein InputStreamReader eignet sich gut für die Umwandlung von äußeren Bytequellen. Wir erinnern uns, dass Java 16 Bit Unicode-Zeichen verwendet, aber viele Computersysteme nur mit 8 Bit ASCII-Zeichen arbeiten. Wenn wir also ein einzelnes Zeichen lesen, muss die passende Konvertierung in das richtige Zeichenformat gesichert sein. Der einfachste Weg ist, ein Zeichen zu lesen und es in ein char – allerdings ohne Konvertierung – zu casten, beispielsweise wie folgt:

FileInputStream fis = new FileInputStream( 
"file.txt" );
DataInputStream dis = new DataInputStream( fis );
char c = (char) dis.readByte();

Da hier keine Konvertierung durchgeführt wird, ist dieser Weg nicht gut. Empfehlenswert ist die Verwendung eines InputStreamReader, der die 8 Bit in ein 16 Bit Zeichen portiert.

FileInputStream fis = new FileInputStream( 
"file.txt" );
InputStreamReader isr = new InputStreamReader( fis );
char c = (char) isr.read();

Im nächsten Kapitel beschreiben wir die Klasse FileReader, die direkt die Datei öffnet und den FileInputStream für uns anlegt.


Galileo Computing

12.5.10 Dateien lesen mit der Klasse FileReader  downtop

Die Klasse FileReader geht direkt aus einem InputStreamReader hervor. Von der Klasse Writer ist bekannt, dass Konstruktoren hinzugefügt werden, damit die Datei geöffnet werden kann, so auch hier.

class java.io.FileReader
extends InputStreamReader

gp  public FileReader( String fileName ) throws FileNotFoundException
Öffnet die Datei über einen Dateinamen zum Lesen. Falls sie nicht vorhanden ist, löst der Konstruktor eine FileNotFoundException aus.
gp  public FileReader( File file ) throws FileNotFoundException
Öffnet die Datei zum Lesen über ein File-Objekt. Falls sie nicht vorhanden ist, löst der Konstruktor eine FileNotFoundException aus.
gp  public FileReader( FileDescriptor fd )
Nutzt die schon vorhandene offene Datei über ein FileDescriptor-Objekt.

Im Folgenden zeigen wir die Anwendung der FileReader-Klasse, die ihren eigenen Quellcode auf den Bildschirm ausgibt. Die Datei muss im korrekten Pfad sein:

Listing 12.19   FileReaderDemo.java
import java.io.*;
public class FileReaderDemo
{
public static void main( String args[] )
{
try
{
FileReader f =
new FileReader( "FileReaderDemo.java" );

int c;

while ( ( c = f.read() ) != -1 )
System.out.print( (char)c );
f.close();

} catch ( IOException e ) {
System.out.println( "Fehler beim Lesen der Datei" );
}
}
}

Galileo Computing

12.5.11 StringReader und CharArrayReader  downtop

Die Klassen StringWriter und CharArrayWriter haben die entsprechenden Lese-Klassen mit den Namen StringReader und CharArrayReader. Beide erlauben das Lesen von Zeichen aus einem String bzw. aus einem Zeichenfeld. Sie leiten sich beide direkt aus Writer ab.

Abbildung

Listing 12.20   StringReaderDemo.java
import java.io.*;

class StringReaderDemo
{
public static void main( String args[] ) throws IOException
{
String s = "Hölle Hölle Hölle";
StringReader sr =
new StringReader( s );
char H = (char) sr.read();
char ö = (char) sr.read();

int c;
while ( (c = sr.read()) != -1 )
System.out.print( (char)c );

sr.close();
}
}
class java.io.StringReader
extends Reader

gp  StringReader( String s )
Erzeugt einen neuen StringReader.
class java.io.CharArrayReader
extends Reader

gp  CharArrayReader( char[] buf )
Erzeugt einen CharArrayReader vom angegebenen Feld.
gp  CharArrayReader( char[] buf, int offset, int length )
Erzeugt einen CharArrayReader vom angegebenen Feld der Länge length und der angegebenen Verschiebung.

Die Klassen StringReader und CharArrayReader überschreiben die Funktionen close(), mark(int), markSupported(), read(), read(char[] cbuf, int off, int len), ready(), reset() und skip(long). Sie unterstützen skip() und mark() bzw. reset().

Tipp Das Zeichenfeld, das CharArrayReader erhält, wird intern nicht kopiert, sondern referenziert. Das heißt, dass nachträgliche Änderungen am Feld, die aus dem Stream gelesenen Zeichen beeinflussen.


Galileo Computing

12.5.12 Schachteln von Eingabe-Streams  downtop

Ebenso wie sich Datenströme in der Ausgabe schachteln lassen, können auch Eingabeströme hintereinander Daten verändern. Folgende Klassen stehen zur Verfügung, die im Konstruktor ein Reader erwarten: BufferedReader, LineNumberReader, FilterReader und PushbackReader. Der Reader wird intern unter der proteced-Variablen in verwaltet.


Galileo Computing

12.5.13 Gepufferte Eingaben mit der Klasse BufferedReader  downtop

Ein BufferedReader puffert ähnlich wie ein BufferedWriter einige Daten vor. Die Daten werden also zuerst in einen kleinen Zwischenspeicher geladen, der wiederum wie beim BufferedWriter 8 k groß ist. Durch die Bereitstellung der Daten müssen weniger Zugriffe auf den Datenträger gemacht werden, und die Geschwindigkeit der Anwendung erhöht sich. Aus BufferedReader geht direkt die Unterklasse LineNumberReader hervor, die Zeilennummern zugänglich macht. Da ein BufferedReader Markierungen und Sprünge erlaubt, werden die entsprechenden Funktionen von Reader überschrieben.

Die Klasse BufferedReader besitzt zwei Konstruktoren. Bei einem lässt sich die Größe des internen Puffers angeben.

class java.io.BufferedReader
extends Reader

gp  BufferedReader( Reader in )
Erzeugt einen puffernden Zeichenstrom mit der Puffergröße von 8 k.
gp  BufferedReader( Reader in, int sz )
Erzeugt einen puffernden Zeichenstrom mit der Puffergröße sz.

Zusätzlich stellt BufferedReader die Methode readLine() zur Verfügung, die eine komplette Textzeile liest und als String an den Aufrufer zurückgibt.

gp  String readLine()
Liest eine Zeile bis zum Zeilenende und gibt den String ohne die Endzeichen zurück. null, wenn der Stream am Ende ist.

Textzeilen lesen mit readLine() – früher und heute

Seit der Java-Version 1.1 ist die Methode readLine() aus der Klasse DataInputStream veraltet. Früher war Folgendes üblich, um eine Textzeile von der Konsole zu lesen:

DataInputStream in = new DataInputStream( 
System.in );
String s = in.readLine();

Heutzutage bietet die Klasse BufferedReader die Methode readLine() an. Die Programme, die den DataInputStream noch für die Zeileneingabe nutzen, sollten mit dem BufferedReader umgeschrieben werden. Somit ergibt sich eine Zeileneingabe von der Konsole nun mit nachfolgenden Zeilen:

BufferedReader in;
in = new BufferedReader( new InputStreamReader(System.in) );
String s = in.readLine();

Eine Textzeile ist durch die Zeichen »\n« oder »\r« begrenzt. Zusätzlich wird auch die Folge der beiden Zeichen beachtet, also »\r\n«. Die Methode basiert auf einer privaten Funktion der Klasse. Sie ruft readLine(boolean skipLF) auf, eine Methode, die auch für uns hin und wieder nützlich wäre. Sie bestimmt, ob die Zeilenendezeichen überlesen werden sollen oder nicht. Im Bedarfsfall bleibt uns nichts anderes übrig, als den Programmcode aus den Originalquellen zu kopieren.

Beispiel Das folgende Programm implementiert ein einfaches Unix »cat«-Kommando. Es können auf der Parameterzeile Dateinamen übergeben werden, die dann in der Standard-Ausgabe ausgegeben werden.

Listing 12.21   cat.java
import java.io.*;

class cat
{
public static void main( String args[] )
{
BufferedReader in;
String line;

for ( int i=0; i < args.length; i++ )
{
try
{
in = new BufferedReader( new FileReader( args[i] ) );

while ( (line = in.readLine()) != null )
System.out.println( line );

} catch ( IOException e ) {
System.out.println( "Error: " + e );
}
}
}
}

Galileo Computing

12.5.14 LineNumberReader zählt automatisch Zeilen mit  downtop

LineNumberReader ist die einzige Klasse, die als Unterklasse von BufferedReader aus den Java-Bibliotheken hervorgeht. Ein LineNumberReader liest die Eingabezeilen und zählt gleichzeitig die Zeilen, die gelesen wurden. Mit zwei Funktionen lässt sich auf die Zeilennummern zugreifen: getLineNumber() und setLineNumber(). Dass die Zeilennummer auch geschrieben werden kann, ist sicherlich ungewöhnlich, aber intern wird nur die Variable lineNumber geschrieben. Bei getLineNumber() wird diese Variable zurückgeliefert. Bei jedem read() untersuchen die Funktionen, ob im Eingabestrom ein »\n« oder »\r« vorkommt. Wenn dies der Fall ist, dann inkrementieren sie die Variable lineNumber.

Abbildung

class java.io.LineNumberReader
extends BufferedReader

gp  int getLineNumber()
Liefert die aktuelle Zeilennummer.
gp  void setLineNumber( int lineNumber )
Setzt die aktuelle Zeilennummer.
Beispiel Die nachfolgende Klasse verbindet einen LineNumberReader mit einer Datei aus einem FileReader. Dann lesen wir die Zeilen mittels readLine() aus. Nun ist es praktisch, dass LineNumberReader eine Erweiterung von BufferedReader ist, die uns diese praktische Funktion vererbt. Wir geben zunächst die Zeilennummer und dann die Zeile selbst aus.

Listing 12.22   LineNumberReaderDemo.java
import java.io.*;

public class LineNumberReaderDemo
{
public static void main( String args[] )
{
try
{
String line;
FileReader fr =
new FileReader("LineNumberReaderDemo.java");

LineNumberReader f =
new LineNumberReader( fr );

while ( (line = f.readLine()) != null )
System.out.println( f.getLineNumber() + ": " + line );

f.close();

} catch ( IOException e ) {
System.out.println("Fehler beim Lesen der Datei");
}
}
}

Galileo Computing

12.5.15 Eingaben filtern mit der Klasse FilterReader  downtop

Wie das Schachteln von Ausgabeströmen, so ist auch das Verbinden mehrerer Eingabeströme möglich. Als abstrakte Basis-Zwischenklasse existiert hier FilterReader, die ein Reader-Objekt im Konstruktor übergeben bekommt. Dieser sichert den Parameter in der protected-Variablen in. Der Konstruktor ist protected, da auch er von der Unterklasse mit super() aufgerufen werden soll. Dazu lässt sich das Beispiel aus dem FilterWriter noch einmal hernehmen. Alle Aufrufe, die an den FilterReader gehen, werden an den Reader in weitergeleitet, das heißt etwa, wenn der FilterReader geschlossen wird, dann wird der Aufruf in.close() ausgeführt. Aus diesem Grunde muss der FilterReader auch alle Methoden von Reader überschreiben, da ja eine Umleitung stattfindet.

Abbildung

abstract class java.io.FilterReader
extends Reader

gp  protected Reader in
Der Zeicheneingabestrom oder null, wenn der Strom geschlossen wurde.
gp  protected FilterReader( Reader in )
Erzeugt einen neuen filternden Reader.

Die Methoden read(), read(char[] cbuf, int off, int len), skip(long n), ready(), markSupported(), mark(int readAheadLimit), reset() und close() werden überschrieben und leiten die Aufrufe direkt an Reader weiter. Wenn dieser eine Exception wirft, wird sie an uns weitergeleitet.

HTML-Tags überlesen mit einem speziellen Filter

Unser nächstes Beispiel ist eine Klasse, die den FilterReader so erweitert, dass HTML-Tags überlesen werden. Sie werden allerdings nicht so komfortabel wie beim HTMLWriter im Datenstrom umgesetzt. Die Klasse überschreibt den notwendigen Konstruktor und implementiert die beiden read()-Methoden. Die read()-Methode ohne Parameter legt einfach ein Ein-Zeichen großes Feld an und ruft dann die read()-Methode auf, die die Daten in ein Feld liest. Da dieser Methode neben dem Feld auch noch die Größe übergeben werden kann, müssen wirklich so viele Zeichen gelesen werden. Es reicht nicht einfach aus, die übergebene Anzahl von Zeichen vom Reader in zu lesen, sondern hier müssen wir beachten, dass eingestreute Tags nicht zählen. Die Zeichenkette <p>Hallo<p> ist demnach fünf Zeichen lang und nicht elf. Liegt eine solche Zeichenkette vor, so müssen mehr als vier Zeichen vom darunter liegenden Reader abgenommen werden.

Listing 12.23   HTMLReader.java
import java.io.*;

class HTMLReader extends FilterReader
{
public HTMLReader( Reader in )
{
super( in );
}

public int read() throws IOException
{
char buf[] = new char[1];
return read(buf, 0, 1) == -1 ? -1 : buf[0];
}

public int
read( char[] cbuf, int off, int len ) throws IOException
{
int numchars = 0;

while ( numchars == 0 )
{
numchars = in.read( cbuf, off, len );

if ( numchars == -1 ) // EOF?
return -1;

int last = off;

for( int i = off; i < off + numchars; i++ )
{
if ( !intag ) {
if ( cbuf[i] == '<' )
intag = true;
else
cbuf[last++] = cbuf[i];
}
else if (cbuf[i] == '>')
intag = false;
}
numchars = last – off;
}
return numchars;
}

private boolean intag = false;
}


public class HTMLReaderDemo
{
public static void main( String args[] )
{
try {
String s = "<html>Hallo! <b>Ganz schön fett.</b>"+
"Ah, wieder normal.</html>";

StringReader sr = new StringReader( s );
HTMLReader hr = new HTMLReader( sr );
BufferedReader in = new BufferedReader( hr );

String t;
while ( (t = in.readLine()) != null )
System.out.println( t );

in.close();
}
catch( Exception e ) { System.err.println( e ); }
}
}

Das Programm produziert dann die einfache Ausgabe:

Hallo! Ganz schön fett. Ah, 
wieder normal.

Der einzige Grund, warum wir auf den HTMLReader noch einen BufferedReader aufsetzen, ist der, dass wir dann die readLine()-Methode nutzen können.


Galileo Computing

12.5.16 Daten zurücklegen mit der Klasse PushbackReader  downtop

Abbildung

Der Eingabefilter PushbackReader ist die einzige Klasse, die direkt aus FilterReader abgeleitet ist. Sie definiert eine Filterklasse, die einen Puffer einer beliebigen Größe besitzt, in den Zeichen wieder zurückgeschrieben werden können. Schreiben wir einen Parser, der eine Wahl auf Grund des nächsten gelesenen Zeichens (ein so genannter Vorrausschauender Parser) trifft, dann kann er dieses Zeichen wieder in den Eingabestrom legen, wenn er den Weg doch nicht verfolgen möchte. Hier ist der Einsatz der Klasse PushbackReader angebracht. Denn der nächste Lesezugriff liest dann dieses zurückgeschriebene Zeichen.

class java.io.PushbackReader
extends FilterReader

gp  PushbackReader( Reader in )
Erzeugt einen PushbackReader aus dem Reader in mit der Puffergröße 1.
gp  PushbackReader( Reader in, int size )
Erzeugt einen PushbackReader aus dem Reader in mit der Puffergröße size.

Um ein Zeichen oder eine Zeichenfolge wieder in den Eingabestrom zu legen, wird die Methode unread() ausgeführt.

gp  public void unread( int c ) throws IOException
public void unread( char cbuf[], int off, int len )
throws IOException
public void unread( char cbuf[] ) throws IOException
Legt ein Zeichen oder ein Feld von Zeichen zurück in den Zeichenstrom.

Zeilennummern entfernen

Das nächste Programm demonstriert die Möglichkeiten eines PushbackReaders. Die Implementierung wirkt möglicherweise etwas gezwungen, sie zeigt jedoch, wie unread() eingesetzt werden kann. Das Programm löst folgendes Problem: Wir haben eine Textdatei (im Programm einfach als String über einen StringReader zur Verfügung gestellt), in der Zeilennummern mit dem String verbunden sind.

134Erste Zeile
234Zeile

Wir wollen nun die Zahlen vom Rest der Zeilen trennen. Dazu lesen wir so lange die Zahlen ein, bis ein Zeichen folgt, wo Character.isDigit() die Rückgabe false ergibt. Dann wissen wir, dass wir keine Ziffer mehr im Strom haben. Das Problem ist nun, dass ja schon ein Zeichen mehr gelesen wurde. In einem normalen Programm ohne die Option, das Zeichen zurücklegen zu können, würde das etwas ungemütlich. Dieses Zeichen müsste dann gesondert behandelt werden, da es ja das erste Zeichen der neuen Eingabe ist und nicht mehr zur Zahl gehört. Doch anstelle dieser Sonderbehandlung legen wir es einfach wieder mit unread() in den Datenstrom und dann kann der nachfolgende Programmcode einfach so weitermachen, als ob nichts gewesen wäre. Dies ist besonders dann von Vorteil, wenn noch Unterprogramme im Einsatz sind, die nach dem Lesen der Zahl eine weitere Funktion aufrufen, die noch einmal alles lesen will. Nach der herkömmlichen Methode muss das gelesene Zeichen dann mit an die Funktion übergeben werden.

Listing 12.24   PushbackReaderDemo.java
import java.io.*;

class PushbackReaderDemo
{
public static void main( String args[] ) throws IOException
{
String s = "134Erste Zeile\n234Zeile";

PushbackReader in =
new PushbackReader(new StringReader(s));

boolean eof = false;
int c;

while ( !eof )
{
try
{
String number = "";

// Lese Zahl bis nichts mehr geht

while ( Character.isDigit((char)(c = in.read())) )
number += (char)c;

if ( c == -1 ) // Ende der Datei => Ende der Schleife
{
eof = true;
continue;
}
else
in.unread( c ); // Letztes Zeichen wieder rein

System.out.print( Integer.parseInt(number) + ":" );

// Hier ist das Zeichen wieder drinne

while ( (c = in.read()) != -1 )
{
System.out.print( (char)c );
if ( c == '\n' )
break;

}
if ( c == -1 ) { // Ende der Schleife
eof = true;
continue;
}
}
catch ( EOFException e )
{
eof = true;
}
}
}
}

Da PushbackReader leider nicht von BufferedReader abgeleitet ist, müssen wir mit einer kleinen Schleife selber die Zeile lesen. Hier muss im Bedarfsfall noch die Zeichenkombination »\n\r« gelesen werden. So wie die Methode jetzt programmiert ist, ist sie eingeschränkt auf Unix-Plattformen, die nur ein einziges Ende-Zeichen einfügen. Doch warum dann nicht readLine()? Wer nun auf die Idee kommt, folgende Zeilen zu schreiben, um doch in den Genuss der Methode readLine() zu kommen, ist natürlich auf dem Holzweg:

StringReader sr = new StringReader( 
s );
BufferedReader br = new BufferedReader ( sr );
PushbackReader in = new PushbackReader( br );
...
br.readLine(); // Achtung, br!!

Wenn wir dem PushbackReader das Zeichen wiedergeben, dann arbeitet der BufferedReader ja genau eine Ebene darüber und bekommt vom Zurückgeben nichts mit. Daher ist es sehr gefährlich, die Verkettung zu umgehen. Im konkreten Fall wird das unread() nicht durchgeführt, und das erste Zeichen nach der Zahl fehlt.


Galileo Computing

12.6 Kommunikation zwischen Threads mit Pipes  downtop

Die Kommunikation zwischen Programmen kann auf vielfältige Weise geschehen. Eine Möglichkeit, die wir bei den Threads kennen gelernt haben, sind statische Variablen oder gemeinsame Datenstrukturen. Bei getrennten Programmen lässt sich die Kommunikation über Dateien realisieren. Auch Datenströme können von einem Teil geschrieben und von einem anderen Teil gelesen werden. Wenn wir aber mit Threads arbeiten, wäre eine Kommunikation über Dateien zu aufwendig, aber ein anderes Stromkonzept praktisch.


Galileo Computing

12.6.1 PipedOutputStream und PipedInputStream  downtop

Einfacher ist der Austausch der Daten über die speziellen Stream-Klassen PipedOuputStream und PipedInputStream, die eine so genannte Pipe bilden. Damit können zwei Threads über Byte-Ströme Informationen austauschen. PipedOuputStream ist eine Unterklasse von OutputStream und kann daher zu allen Ausgabeströmen wie DataOutputStream ausgebaut werden. Das Gleiche gilt für PipedInputStream. Eine Pipe zwischen zwei Threads wird durch die Verbindung eines PipedOutputStream mit einem PipedInputStream eingerichtet und umgekehrt. Hierzu gibt es mehrere Varianten. Beide Konstruktoren gibt es in doppelter Ausführung: Entweder als Standard-Konstruktor oder als Konstruktor, der den jeweils anderen Stream aufnimmt.

Beispiel Verbinde den Eingabe-Stream pis mit dem Ausgabe-Stream pos
PipedInputStream  pis = new PipedInputStream();
PipedOutputStream pos = new PipedOutputStream( pis );

Werden jetzt Daten produziert und in den pos gepackt, dann werden sie nach pis geschickt. Da wir den PipedOutputStream mit einem Eingabestrom verbunden haben, ist ein einseitiger Verbindungskanal aufgebaut. An pis lassen sich mit read() die Daten entnehmen.

Bei einer bidirektionalen Verbindung müssen wir natürlich beide Seiten miteinander verbinden. Die Klassen PipedOutputStream und PipedInputStream bieten eine Methode connect() an, mit der nachträglich das zweite Paar gebildet werden kann.

Abbildung

Beispiel Ein PipedOutputStream soll mit einem PipedInputStream doppelseitig verbunden werden.

PipedOutputStream pos = new PipedOutputStream();
PipedInputStream pis = new PipedInputStream();
po.connect( pis );
pi.connect( pos );

Interna

Die Daten, die vom PipedOutputStream mittels write() geschrieben werden, gelangen direkt ohne Pufferung zum Eingabestrom. Werfen wir einen kurzen Blick auf die skizzierte Implementierung:

class PipedOutputStream extends 
OutputStream
{
private PipedInputStream sink;

public PipedOutputStream( PipedInputStream snk )
throws IOException
{
// Fehlerbehandlung
sink = snk;
snk.in = -1;
snk.out = 0;
snk.connected = true;
}

public void write(int b) throws IOException
{
if (sink == null)
throw new IOException("Pipe not connected");

sink.receive(b);
}
}

Der Eingabestrom ist etwas anders konstruiert, denn er nutzt intern einen Puffer von 1024 Zeichen (512 bei Kaffe). Das bedeutet, der Schreibende kann bis zu 1024 Bytes produzieren, bis die Kommunikation stoppen muss. Denn mit dieser Größe ist der Puffer voll und der Lesende muss den Puffer leeren, damit der Konsument wieder etwas produzieren kann. Erst wenn der Puffer entleert wurde, kann es weitergehen. Umgekehrt heißt das, dass der lesende Thread bei ungenügend vielen Zeichen warten muss, bis der Schreiber die nötige Anzahl hinterlegt hat. Dazu wird intern mittels Thread-Synchronisation gearbeitet.


Galileo Computing

12.6.2 PipedWriter und PipedReader  downtop

Die Klassen PipedWriter und PipedReader sind die char-Varianten für die sonst byte-orientierten Klassen PipedOutputStream und PipedInputStream. Diese sollen uns für ein Beispiel dienen. Zwei Threads arbeiten miteinander und tauschen Daten aus. Der eine Thread produziert Zufallszahlen, die ein anderer auf dem Bildschirm darstellt.

Listing 12.25   PipeDemo.java
import java.io.*;

class RandomWriter extends Thread
{
private PipedWriter out;

RandomWriter()
{
out = new PipedWriter();
}

PipedWriter getPipedWriter()
{
return out;
}

public void run()
{
PrintWriter pw = new PrintWriter( out );

for ( int i=0; i<10; i++ )
pw.println( Math.random() );
}
}

class RandomReader extends Thread
{
private PipedReader in;

RandomReader( PipedWriter out ) throws IOException
{
in = new PipedReader( out );
}

public void run()
{
BufferedReader br = new BufferedReader( in );

while ( true )
try
{
System.out.println( br.readLine() );
} catch ( IOException e ) { }
}
}

public class PipeDemo
{
public static void main( String args[] ) throws Exception
{
RandomWriter rw = new RandomWriter();
RandomReader rr = new RandomReader( rw.getPipedWriter() );

rr.start();
rw.start();
}
}
Datenkompression

Um die Größe einer Datei zu verringern, damit sie weniger Platz auf dem Datenträger einnimmt, wird sie komprimiert. Ein anderer Grund für die Kompression ist, mehrere Dateien zu einem Archiv zusammenzufassen. Bei Netzwerkverbindungen ist die logische Konsequenz, dass weniger Daten natürlich auch schneller übertragen sind.

Über alle Plattformen hinweg haben sich Standards gebildet. Davon sollen zwei Anwendungen an dieser Stelle beschrieben werden.

GZIP und GUNZIP

Seitdem der LZW-Algorithmus im Juni 1984 im IEEE Journal beschrieben wurde, gibt es unter jedem UNIX-System die Dienstprogramme compress und uncompress, die verlustfrei Daten zusammenpacken. Über dieses Format wird ein Datenstrom gepackt und entpackt. gzip und gunzip sind freie Varianten von compress bzw. uncompress und unterliegen der GNU Public Licence. Das Format enthält eine zyklische Überprüfung gegen defekte Daten. Die Endung einer Datei, die mit gzip gepackt ist, ist mit ».gz« angegeben, wobei die Endung unter compress nur ».Z« ist. gzip behält die Rechte und Zeitattribute der Datei bei.

Komprimieren mit tar?

tar ist kein Programm, mit dem sich Dateien komprimieren lassen. tar verpackt lediglich mehrere Dateien zu einer neuen Datei, ohne sie zu komprimieren. Oftmals werden die mit tar gepackten Dateien anschließend mit compress bzw. gzip gepackt. Die Endung ist dann ».tar.Z«. Werden mehrere Daten erst in einem tar-Archiv zusammengefasst und dann gepackt, ist die Kompressionsrate höher, als wenn jede Datei einzeln komprimiert wird. Die Grund ist einfach, denn das Kompressionsprogramm kann die Redundanz besser ausnutzen. Der Nachteil ist freilich, dass für eine Datei gleich das ganze tar-Archiv ausgepackt werden muss.

Zip

Das Utility zip erzeugt ein Archiv aus mehreren Dateien. Der Unterschied zu gzip liegt darin, dass zip kein Filterprogramm mit einem Datenstrom ist, sondern ein Programm, welches sich Dateien nimmt, die zu einem Archiv zusammengebunden werden. Auf jede Datei lässt sich anschließend individuell zugreifen. PkZip ist unter MS-DOS ein Standardprogramm, unter Windows oft WinZip. Obwohl Zip und GZip von der Anwendung her unterschiedlich arbeiten, verwenden sie denselben Algorithmus.

Es gibt auch unkomprimierte Zip-Archive, obwohl diese selten sind. Ein Beispiel dafür sind die Java-Archive des Internet Explorers. Die größte Datei ist unkomprimiert 5,3 MB groß, gepackt wäre sie 2 MB groß. Sie wurden vermutlich aus Gründen der Geschwindigkeit nicht gepackt, da sich die Daten aus unkomprimierten Archiven schneller lesen lassen, da keine Prozessorleistung für das Entpacken aufgewendet werden muss.

Die Java-Unterstützung beim Komprimieren

Unter Java ist eine Paket java.util.zip eingerichtet, um mit komprimierten Dateien zu operieren. Das Paket bietet zur Komprimierung zwei allgemein gebräuchlich Formate: GZIP/GUNZIP zum Komprimieren bzw. Entkomprimieren für Datenströme und ZIP zum Behandeln von Archiven und Komprimieren von Dateien. Beide basieren auf Algorithmen, die im RFC 1952 definiert sind.


Galileo Computing

12.6.3 Datenströme komprimieren  downtop

Zum Packen und Entpacken von Strömen wird GZIP verwendet. Wir sehen uns nun einige Datenströme an, die auf der Klasse FilterOutputStream basieren.

Daten packen

Die Klasse java.util.zip bietet zwei Unterklassen von FilterOutputStream, die das Schreiben von komprimierten Daten erlauben. Um Daten unter dem GZIP-Algorithmus zu packen, müssen wir einfach einen vorhandenen Datenstrom zu einem GZIPOutputStream erweitern.

FileOutputStream out = new FileOutputStream( 
Dateiname );
GZIPOutputStream zipout =
new GZIPOutputStream( out );
class java.util.zip.GZIPOutputStream
extends DeflaterOutputStream

gp  GZIPOutputStream( OutputStream out)
Erzeugt einen packenden Datenstrom mit der voreingestellten Puffergröße von 512 Byte.
gp  GZIPOutputStream( OutputStream out, int size )
Erzeugt einen packenden Datenstrom mit einem Puffer der Größe size.
Beispiel Eine Datei nach dem GZIP-Format packen. Das Programm verhält sich wie das unter UNIX bekannte gzip.

Abbildung

Listing 12.26   gzip.java
import java.io.*;
import java.util.zip.*;

class gzip
{
public static void main( String args[] )
{
if ( args.length != 1 ) {
System.out.println( "Usage: gzip source" );
return;
}
String zipname = args[0] + ".gz";
try
{
GZIPOutputStream zipout =
new GZIPOutputStream( new FileOutputStream( zipname ) );

byte buffer[] = new byte[blockSize];
FileInputStream in = new FileInputStream( args[0] );
int length;

while ( (length = in.read(buffer, 0, blockSize)) != -1 )
zipout.write( buffer, 0, length );

in.close();
zipout.close();
}
catch ( IOException e )
{
System.out.println( "Error: Couldn't compress "+args[0] );
}
}
private static int blockSize = 8192;
}

Zunächst überprüfen wir, ob ein Argument auf der Kommandozeile vorhanden ist. Aus diesem Argument konstruieren wir mit der Endung ».gz« einen FileOutputStream. Um diesen manteln wir dann noch einen GZIPOutputStream. Mittels read() lesen wir aus dem FileInputStream einen Block Daten und schreiben ihn in den GZIPOutputStream, der die Daten dann komprimiert.

Daten entpacken

Um die Daten zu entpacken, müssen wir nur den umgekehrten Weg beschreiten. Zum Einsatz kommen hier eine der zwei Unterklassen von FilterInputStream. Wieder wickeln wir um einen InputStream einen GZIPInputStream und lesen dann daraus.

class java.util.zip.GZIPInputStream
extends InflaterInputStream

gp  GZIPInputStream( InputStream in, int size )
Erzeugt einen auspackenden Datenstrom mit einem Puffer der Größe size.
gp  GZIPInputStream( InputStream in )
Erzeugt einen auspackenden Datenstrom mit der voreingestellten Puffergröße von 512 Byte.
Beispiel Eine Anwendung, die sich so verhält wie das unter UNIX bekannte gunzip.

Listing 12.27   gunzip.java
import java.io.*;
import java.util.zip.*;

public class gunzip
{
public static void main( String args[] )
{
if ( args.length != 1 ) {
System.out.println( "Usage: gunzip source" );
return;
}

String zipname, source;
if ( args[0].endsWith(".gz") ) {
zipname = args[0];
source = zipname.substring(0, zipname.length() – 3);
}
else {
zipname = args[0] + ".gz";
source = args[0];
}
try
{
GZIPInputStream zipin =
new GZIPInputStream( new FileInputStream( zipname ) );

byte buffer[] = new byte[blockSize];
FileOutputStream out = new FileOutputStream( source );
int length;
while ( (length = zipin.read(buffer, 0, blockSize)) != -1 )
out.write( buffer, 0, length );
out.close();
zipin.close();
}
catch ( IOException e ) {
System.out.println("Error: Couldn't decompress "+args[0]);
}
}
private static int blockSize = 8192;
}

Endet die Datei mit ».gz«, so entwickeln wir daraus den herkömmlichen Dateinamen. Endet sie nicht mit diesem Suffix, so nehmen wir einfach an, dass die gepackte Datei diese Endung besitzt, der Benutzer dies aber nicht angegeben hat. Nach dem Zusammensetzen des Dateinamens holen wir von der gepackten Datei einen FileInputStream und packen einen GZIPInputStream darum. Nun öffnen wir die Ausgabedatei und schreiben in Blöcken zu 8 k die Datei vom GZIPInputStream in die Ausgabedatei.


Galileo Computing

12.6.4 Zip-Archive  downtop

Der Zugriff auf die Daten eines Zip-Archivs unterscheidet sich schon deshalb vom Zugriff auf die Daten eines GZip-Streams, weil diese in Form eines Archivs vorliegen. Beim Packen einer Datei oder eines ganzen Verzeichnisses kann hier der Packalgorithmus bessere Ergebnisse erzielen, als wenn alle Dateien einzeln gepackt würden. Jede Datei in einem Verzeichnis ist also durch einen Kompressionsbaum entstanden, der sich durch die anderen Dateien ergibt. Wir müssen uns also damit beschäftigen, wie wir auf das Archiv und auf die Daten des Archivs zugreifen können.

Die Klasse ZipFile und ZipEntry

Objekte der Klasse ZipFile repräsentieren ein Zip-Archiv und bieten Funktionen, um auf die einzelnen Dateien (Objekte der Klasse ZipEntry) des Archivs zuzugreifen. Intern nutzt ZipFile eine Datei mit wahlfreiem Zugriff (Random Access File), sodass wir auf spezielle Einträge sofort zugreifen können. Ein ZIP-Archiv der Reihe nach auszulesen, so wie ein gepackter Strom es vorschreibt, ist überflüssig.

Unter Java ist jeder Eintrag in einem ZIP-Archiv durch ein Objekt der Klasse ZipEntry repräsentiert. Liegt einmal ein ZipEntry-Objekt vor, so können von ihm durch verschiedene Methoden Dateiattribute entlockt werden, beispielsweise die Originalgröße, das Kompressionsverhältnis, das Datum, wann die Datei angelegt wurde und weiteres. Auch kann ein Datenstrom erzeugt werden, sodass sich eine komprimierte Datei im Archiv lesen lässt.

»Öffentliche Eigenschaften von ZipFile und ZipEntry« ]

Um auf die Dateien eines Archivs zuzugreifen, muss ein ZipFile-Objekt erzeugt werden, was auf zwei Arten geschehen kann: Entweder über den Dateinamen oder über ein File-Objekt. Es gibt drei Konstruktoren für Zip-Archive: Ein Standard-Konstruktor ist protected und kann daher nicht öffentlich verwendet werden. Bei den beiden anderen muss des Weiteren IOException und ZipException abgefangen werden.

Abbildung

class java.util.zip.ZipFile
implements java.util.zip.ZipConstants

gp  ZipFile( String ) throws ZipException, IOException
Öffnet ein Zip-Archiv mit dem Dateinamen.
gp  ZipFile( File ) throws ZipException, IOException
Öffnet ein Zip-Archiv mit dem gegebenen File-Objekt.

Anschließend lässt sich eine Enumeration mit der Methode entries() erzeugen und alle Dateien als ZipEntry zurückgeben.

Nachfolgend sehen wir im Programmbeispiel, wie eine Iteration durch die Einträge des Archivs aussehen kann.

ZipFile z = new ZipFile( "foo.zip" 
);

Enumeration e = z.entries();
while ( e.hasMoreElements() )
{
ZipEntry ze =
(ZipEntry)e.nextElement();
System.out.println( ze.getName() );
}

Neben der Enumeration gibt es noch eine weitere Möglichkeit, um an bestimmte Einträge heranzukommen: getEntry(String). Ist der Name der komprimierten Datei bekannt, gib es sofort ein ZipEntry-Objekt zurück.

Wollen wir nun die gesuchte Datei auspacken, holen wir mittels getInputStream(ZipEntry) ein InputStream-Objekt und können dann auf den Inhalt der Datei zugreifen. Es ist bemerkenswert, dass getInputStream() keine Methode von ZipEntry ist, so wie wir es erwarten würden, sondern von ZipFile, obwohl dies mit den eigentlichen Dateien nicht viel zu tun hat.

Beispiel Liegt im Archiv foo.zip die gepackte Datei largeFile, dann gelangen wir mit Folgendem an deren Inhalt:
ZipFile file = new ZipFile( "foo.zip" 
);
ZipEntry entry = file.getEntry( "largeFile" );
InputStream input = file.getInputStream( entry );
Der InputStream liefert dann den entpackten Inhalt der Datei.

class java.util.zip.ZipFile
implements java.util.zip.ZipConstants

gp  ZipEntry getEntry( String name )
Liefert eine Datei aus dem Archiv. null, wenn kein Eintrag mit dem Namen existiert.
gp  InputStream getInputStream( ZipEntry ze ) throws IOException
Gibt einen Eingabestrom zurück, mit dem auf den Inhalt einer Datei zugegriffen werden kann.
gp  String getName()
Liefert den Pfadnamen des Zip-Archivs.
gp  Enumeration entries()
Gibt eine Aufzählung des Zip-Archivs zurück.
gp  int size()
Gibt die Anzahl der Einträge im Zip-Archiv zurück.
gp  void close() throws IOException
Schließt das Zip-Archiv.
gp  ZipEntry createZipEntry( String name )
Erzeugt ein neues ZipEntry-Objekt mit dem angegebenen Dateinamen.

Eine Funktion, die eine Datei auspackt

Um die Datei tatsächlich auszupacken, müssen wir nur noch eine neue Datei erzeugen, diese mit einem Datenstrom verbinden und dann die dekomprimierte Ausgabe dahin umleiten. Eine kompakte Funktion getEntry(ZipFile, ZipEntry), die auch noch aus Geschwindigkeitsgründen einen BufferedInputStream bzw. BufferedOutputStream um die Kanäle packt, kann so aussehen:

public static void getEntry( ZipFile 
zipFile, ZipEntry target )
throws ZipException,IOException
{
try {
File file = new File( target.getName() );
BufferedInputStream bis = new BufferedInputStream(
zipFile.getInputStream( target ) );
File dir = new File( file.getParent() );
dir.mkdirs();
BufferedOutputStream bos = new BufferedOutputStream(
new FileOutputStream( file ) );
int c;
while ( ( c = bis.read() ) != EOF )
bos.write( (byte)c );

bos.close();
}
}

Das Objekt ZipEntry und die Datei-Attribute

Ein Objekt der Klasse ZipEntry repräsentiert jeweils eine Datei oder ein Verzeichnis eines Archivs. Diese Datei kann gepackt (dafür ist die Konstante ZipEntry.DEFLATED reserviert) oder auch ungepackt sein (angezeigt durch die Konstante ZipEntry.STORED). Auf dem Objekt können verschiedene Attribute gesetzt und abgefragt werden. Dadurch lassen sich Statistiken über Kompressionsraten und Weiteres ermitteln. Entsprechend den folgenden Funktionen überschreibt ZipEntry auch die Funktionen toString(), hashCode() und clone() der Klasse Object:

class java.util.zip.ZipEntry
implements java.util.zip.ZipConstants, Cloneable

gp  String getName()
Liefert den Namen des Eintrags.
gp  void setTime( long time )
Ändert die Modifikationszeit des Eintrags.
gp  long getTime()
Liefert die Modifikationszeit des Eintrags oder –1, wenn diese nicht angegeben ist.
gp  void setSize( long size )
Setzt die Größe der unkomprimierten Datei. Wir werden mit einer IllegalArgument Exception bestraft, wenn die Größe kleiner 0 oder größer 0xFFFFFFFF ist.
gp  long getSize()
Liefert die Größe der unkomprimierten Datei oder –1, falls unbekannt.
gp  long getCrc()
Liefert die CRC-32 Checksumme der unkomprimierten Datei oder –1, falls unbekannt.
gp  void setMethod( int method )
Setzt die Kompressionsmethode entweder auf STORED oder DEFLATED.
gp  int getMethod()
Liefert die Kompressionsmethode entweder auf STORED, DEFLATED oder–-1, falls unbekannt.
gp  void setExtra( byte[] extra )
Setzt das optionale Zusatzfeld für den Eintrag. Übersteigt die Größe des Zusatzfelds 0xFFFFF Bytes, dann wird eine IllegalArgumentException ausgelöst.
gp  byte[] getExtra()
Liefert das Extrafeld oder null, falls es nicht belegt ist.
gp  setComment( String comment )
Setzt einen Kommentar-String der 0xFFFF Zeichen lang sein darf (sonst wird eine IllegalArgumentException ausgelöst).
gp  String getComment()
Gibt denn Kommentar zurück oder null.
gp  long getCompressedSize()
Liefert die Dateigröße nach dem Komprimieren oder –1, falls diese unbekannt ist. Ist der Kompressionstyp ZipEntry.STORED, dann stimmt diese Größe natürlich mit dem Rückgabewert von getSize() überein.
gp  boolean isDirectory()
Liefert true, falls der Eintrag ein Verzeichnis ist. Der Name der Datei endet mit einem Slash '/'.

Dateien und Attribute als Inhalte eines Archivs

Wir haben nun die Informationen, um uns den Inhalt eines Archivs mit den Attributen anzeigen zu lassen. Wir wollen im Folgenden zwei Klassen entwickeln, die dies umsetzen. Zunächst die allgemeine Klasse ZIPList:

Listing 12.28   ZIPList.java
import java.io.*;
import java.text.*;
import java.util.*;
import java.util.zip.*;

class ZIPList
{
public ZIPList( File file ) throws IOException
{
ZipFile zipfile = new ZipFile( file );

s = new String[ zipfile.size() ];

int i = 0;
for ( Enumeration e = zipfile.entries();
e.hasMoreElements(); )
s[i++] = buildInfoString( (ZipEntry) e.nextElement() );

// for sorting the entries

collator = Collator.getInstance( Locale.GERMANY );
collator.setStrength( Collator.PRIMARY );
}

public void sort()
{
Arrays.sort( s, collator );
}

public String[] getFileList()
{
return s;
}

// private

private String buildInfoString( ZipEntry entry )
{
String fileName = entry.getName();

long size = entry.getSize(),
compressedSize = entry.getCompressedSize(),
time = entry.getTime();

SimpleDateFormat format = new SimpleDateFormat();
String outTime = format.format( new Date(time) );

return (entry.isDirectory() ? "+" : "-") + fileName
+ "\tSize: " + size
+ "\tPacked: " + compressedSize
+ "\t" + outTime;
}

private Collator collator;

private String s[];
}

Die Klasse enthält einen Konstruktor, der den Dateinamen des Archivs verlangt. Intern legt dieser ein Array von Zeichenketten an, welches alle Namen der Dateien im Archiv aufnimmt. Nach dem Aufruf enthält das Feld alle Dateien, und die Methode getFileList() gibt dieses Feld nach außen weiter. Den Weg, die Daten sofort auszugeben, haben wir hier absichtlich nicht gewählt, denn über sort() lassen sich die Einträge noch sortieren. Wir nutzen hier die statische Funktion Arrays.sort(), um ein Feld von Strings zu sortieren. Damit diese auch korrekt nach der deutschen Schreibweise einsortiert werden, nutzen wir einen Collator, der mit dem Exemplar von Locale.GERMANY initialisiert wird.

Der Konstruktor baut das Feld mit den Datei- und Verzeichnisnamen durch die Enumeration auf. Dabei wird für jedes ZipEntry eine private Funktion buildInfoString() genutzt. Sie baut einen primitiven String zusammen, der Dateiname, Größe, Packrate und Datum anzeigt. Daneben werden alle Verzeichnisse mit einem Pluszeichen markiert und Dateien mit einem Minuszeichen. Die Konsequenz dieser Notation ist, dass Verzeichnisse bei der Gesamtanzeige zuerst ausgegeben werden.

Die zweite Klasse ist ZIPListDemo. Sie öffnet eine Datei über die Kommandozeile und beschwert sich falls der Parameter fehlt. Existiert die Datei nicht, so wird genauso eine Fehlermeldung ausgegeben wie beim nicht erteilten Zugriff. Der Dateiname wird zu ZIPList() weitergereicht und die Daten dann sortiert ausgegeben:

Listing 12.29   ZIPListDemo
class ZIPListDemo
{
public static void main( String args[] ) throws IOException
{
String fileName = null;

if ( args.length != 1 )
fileName = args[0];

File file = new File( fileName );

if ( !file.isFile() ) {
System.out.println( "Error: file \"" + fileName +
"\" not found." );
System.exit(1);
}
if ( !file.canRead() ) {
System.out.println("Error: file \"" + fileName +
"\" cannot be read.");
System.exit(1);
}
ZIPList l = new ZIPList( file );

l.sort();

String s[] = l.getFileList();

for ( int i=0; i < s.length; i++ )
System.out.println( s[i] );
}
}

Ein ganzes Archiv Datei für Datei entpacken

Auch ein Programm zum Entpacken des gesamten Zip-Archivs ist nicht weiter schwierig. Wir müssen nur mit einer Enumeration durch das Archiv laufen und dann für jeden Eintrag eine Datei erzeugen. Dazu nutzen wir eine modifizierte Version von getEntry() aus dem vorangehenden Kapitel. Die Methode saveEntry(ZipFile, ZipEntry) muss, wenn sie alle Dateien ordnungsgemäß auspacken soll, erkennen, ob es sich bei der Datei um ein Verzeichnis handelt oder nicht. Dazu verwenden wir die Funktion isDirectory() des ZipEntry- Objekts. Denn diese Funktion versichert uns, dass es sich um ein Verzeichnis handelt, und wir daher einen Ordner mittels mkdirs() anlegen müssen und keine Datei. Wenn es allerdings eine Datei ist, so verhält sich saveEntry() wie getEntry().

Listing 12.30   UnZip.java
import java.util.zip.*;
import java.io.*;
import java.util.*;

public class UnZip
{
public static final int EOF = -1;
public static void main( String args[] )
{
Enumeration enum;
if ( argv.length == 1 )
{
try {
ZipFile zf = new ZipFile( argv[0] );
enum = zf.entries();
while ( enum.hasMoreElements() )
{
ZipEntry target = (ZipEntry)enum.nextElement();
System.out.print( target.getName() + " ." );
saveEntry( zf, target );
System.out.println( ". unpacked" );
}
}
catch( FileNotFoundException e ) {
System.out.println( "zipfile not found" );
}
catch( ZipException e ) {
System.out.println( "zip error..." );
}
catch( IOException e ) {
System.out.println( "IO error..." );
}
}
else
System.out.println( "Usage:java UnZip zipfile" );
}
public static void saveEntry( ZipFile zf, ZipEntry target )
throws ZipException,IOException
{
try
{
File file = new File( target.getName() );
if ( target.isDirectory() )
file.mkdirs();
else
{
InputStream is = zf.getInputStream( target );
BufferedInputStream bis = new BufferedInputStream( is );
File dir = new File( file.getParent() );
dir.mkdirs();
FileOutputStream fos = new FileOutputStream( file );
BufferedOutputStream bos = new BufferedOutputStream(fos);
int c;
while ( ( c = bis.read() ) != EOF ) // oder schneller
bos.write( (byte)c );
bos.close();
fos.close();
}
}
}
}

Kompressionsgrad einer Zip-Datei

Wird eine Datei über einen ZipOutputStream erzeugt, lässt sich die Kompressionsrate über die Methode setLevel(int) einstellen. Das Level ist eine Zahl zwischen 0 und 9. Die Kompression übernimmt ein Deflater-Objekt, welches im DeflaterOutputStream (die Oberklasse von ZipOutputStream) verwaltet wird. So ruft ZipOutputStream lediglich vom Deflater die Methode setLevel() auf.


Galileo Computing

12.7 Prüfsummen  downtop

Damit Fehler bei Dateien oder bei Übertragungen von Daten auffallen, werden Prüfsummen (engl. Checksums) gebildet. Prüfsummen werden vor der Übertragung erstellt und mit dem Paket versendet. Der Empfänger berechnet diese Prüfsumme neu und vergleicht sie mit dem übertragenen Wert. Stimmt der berechnete Wert mit dem übertragenen Wert überein, so war die Übertragung höchstwahrscheinlich in Ordnung. Es ist ziemlich unwahrscheinlich, dass eine Änderung von Bits nicht auffällt. Genauso werden korrupte Archive erkannt. Pro Datei wird eine Prüfsumme berechnet. Soll die Datei entpackt werden, so errechnen wir wieder die Summe. Ist diese fehlerhaft, so muss die Datei fehlerhaft sein. (Wir wollen hier ausschließen, dass zufälligerweise die Prüfsumme fehlerhaft ist, was natürlich auch passieren kann.)


Galileo Computing

12.7.1 Die Schnittstelle Checksum  downtop

Wir finden Zugang zur Prüfsummen-Berechnung über die Schnittstelle java.util.zip .Checksum, die für ganz allgemeine Prüfsummen steht. Eine Prüfsumme wird entweder für ein Feld oder ein Byte berechnet. Checksum liefert die Schnittstelle zum Initialisieren und Auslesen von Prüfsummen, die von konkreten Prüfsummen-Klassen implementiert werden muss.

interface java.util.zip.Checksum

gp  long getValue()
Liefert die aktuelle Prüfsumme.
gp  void reset()
Setzt die aktuelle Prüfsumme auf einen Anfangswert.
gp  void update( int b )
Aktualisiert die aktuelle Prüfsumme mit b.
gp  void update( byte b[], int off, int len )
Aktualisiert die aktuelle Prüfsumme mit dem Feld.

Bisher finden sich in den Java-Bibliotheken nur die Klassen CRC32 und Adler32, die von der Schnittstelle Checksum Gebrauch machen. Aber mit wenig Aufwand lässt sich beispielsweise eine Klasse schreiben, die die einfache Paritätsüberprüfung übernimmt. Dies können wir zum Beispiel bei der Übertragung von Daten an der seriellen Schnittstelle verwenden. (Glücklicherweise ist dies im Fall der seriellen Schnittstelle schon in der Hardware implementiert.)


Galileo Computing

12.7.2 Die Klasse CRC32  downtop

Oft werden Prüfsummen durch Polynome gebildet. Die Prüfsumme, die für Dateien verwendet wird, heißt CRC32 und das bildende Polynom lautet:

x32  +x26  +x23  +x22  +x16  +x12  +x11  +x10  +x +x +x +x +x +x+1

Nun lässt sich zu einer 32 Bit-Zahl eine Prüfsumme berechnen, die für genau diese 4 Bytes stehen. Damit bekommen wir aber noch keinen ganzen Block kodiert. Um das zu erreichen, berechnen wir den Wert eines Zeichens und Xor-verknüpfen den alten CRC-Wert mit dem neuen. Jetzt lassen sich beliebig Blöcke sichern. Ohne groß zu überlegen, dürfte klar sein, dass viel Zeit für die Berechnung aufgewendet werden muss. Daher ist der mathematische Algorithmus auch nicht in Java, sondern in C implementiert. Er nutzt Tabellen, um möglichst schnell zu sein.

Beispiel CRC32 berechnet eine Prüfsumme entweder für ein Byte oder für ein Feld.

Kurz und knapp sieht ein Programm zur Berechnung von Prüfsummen für Dateien dann so aus (in ist ein InputStream-Objekt):

CRC32 crc = new CRC32();
byte ba[] = new byte[(int)in.available()];
in.read( ba );
crc.
update( ba );
in.close();

CRC32 implementiert nicht nur alle Methoden, sondern fügt noch zwei Funktionen und natürlich einen Konstruktor hinzu.

Abbildung

class java.util.zip.CRC32
implements Checksum

gp  CRC32()
Erzeugt ein neues CRC32-Objekt mit der Start-Prüfsumme 0.
gp  loggetValue()
Liefert den CRC32-Wert.
gp  voidreset()
Setzt die interne Prüfsumme auf 0.
gp  voidupdate( byte[] b )
Aktualisiert die Prüfsumme mit dem Feld, durch Aufruf von update(b, 0, b.length).
gp  voidupdate( int b )
Implementiert update() aus Checksum für ein Byte. Nativ implementiert.
gp  voidupdate( byte[] b, int off, int len )
Implementiert update() aus Checksum für ein Feld. Nativ implementiert.

CRC eines Datenstroms berechnen

Wir wollen nun ein kleines Testprogramm entwickeln, mit dem wir die CRC32 eines Datenstroms berechnen. Dazu schreiben wir die Methode crc32(), die einen InputStream erwartet. Anschließend werden so lange Bytefolgen ausgelesen, bis available() Null liefert. Für unser Testprogramm, welches einen FileInputStream liefert, wird available() die Dateigröße liefern. Bei großen Dateien ist es sicherlich angebracht Blöcke einzulesen, die dann mit der crc.update(byte[])-Methode verarbeitet werden.

Listing 12.31   CRC32Demo.java
import java.io.*;
import java.util.*;
import java.util.zip.*;

class CRC32Demo
{
static long crc32( InputStream in ) throws IOException
{
CRC32 crc = new CRC32();
int blockLen;

while ( (blockLen=(int)in.available()) > 0 )
{
byte ba[] = new byte[blockLen];
in.read( ba );
crc.update( ba );
}
in.close();

return crc.getValue();
}

static public void main(String args[]) throws IOException
{
InputStream is
is = new FileInputStream( new File("c:\\readme.txt") );

System.out.println( crc32(is) );

System.in.read();
}
}

Einen Datenstrom mit gleichzeitiger CRC-Berechnung

Auch das Dienstprogramm Jar – ein Javaprogramm unter sun.tools.jar – macht Gebrauch von der CRC32-Klasse. Wir finden hier etwas ganz Interessantes im Quellcode wieder, und zwar einen Ausgabestrom, der nicht Daten schreibt, sondern nur die Prüfsumme berechnet. Für den eigenen Gebrauch ist es sicherlich spannender, einen Datenstrom über einen FilterOutputStream so zu implementieren, dass auch Daten gleich geschrieben werden. Der nachfolgende Auszug zeigt die wesentlichen Schritte. Nun müssen wir nur noch einen Konstruktor schreiben, der sich den OutputStream in out merkt, und dann werden die Daten in diesen Strom geschrieben.

Listing 12.32   CRC32OutputStream.java
class CRC32OutputStream extends 
FilterOutputStream
{
public CRC32OutputStream( OutputStream out )
{
super( out );
}

public void write( int i ) throws IOException
{
crc.update( i );
out.write( i );
}

public void write( byte b[] ) throws IOException
{
crc.update( b, 0, b.length );
out.write( b, 0, b.length );
}

public void write( byte b[], int off, int len )
throws IOException
{
crc.update( b, off, len );
out.write( b, off, len );
}

private CRC32 crc = new CRC32();
}

Wir hätten in unserem Programm natürlich wieder auf die Implementierung der beiden write()-Methoden mit Feldern verzichten können, da der FilterOutputStream eine Umleitung macht, doch diese ist ja mit dem bekannten Geschwindigkeitsverlust verbunden. Da wir nicht wollen, dass jedes einzelne Byte geschrieben und mit einer Prüfsumme versehen wird, gönnen wir uns die paar Zeilen mehr.


Galileo Computing

12.7.3 Die Adler32-Klasse  downtop

Diese Klasse ist eine weitere Klasse, mit der sich eine Prüfsumme berechnen lässt. Doch warum zwei Verfahren? Ganz einfach. Die Berechnung von CRC32-Prüfsummen kostet – obwohl in C(++) programmiert – viel Zeit. Die Adler32-Prüfsumme lässt sich wesentlich schneller berechnen und bietet ebenso eine geringe Wahrscheinlichkeit, dass Fehler unentdeckt bleiben. Der Algorithmus heißt nach seinem Programmierer Mark Adler und ist eine Erweiterung des Fetcher -Algorithmus, definiert im ITU-T X.224/ISO 8073 Standard, auf 32 Bit-Zahlen. Die Adler32-Prüfsumme setzt sich aus zwei Summen für ein Byte zusammen. s1 ist die Summe aller Bytes und s2 die Summe aller s1. Beide Werte werden Modulo 65521 genommen. Am Anfang ist s1=1 und s2=0. Die Adler32-Prüfsumme speichert den Wert als s2*65536 + s1 in der MSB (Most-Significant-Byte First, Netzwerk-Reihenfolge).

Eine Beschreibung der Kompression und des Adler32-Algorithmus findet sich im Internet Draft »ZLIB Compressed Data Format Specification version 3.3«.

class java.util.zip.Adler32
implements Checksum

gp  Adler32()
Erzeugt ein neues Adler32-Objekt mit der Start-Prüfsumme 1.
gp  getValue()
Liefert den Adler32-Wert.
gp  reset()
Setzt die interne Prüfsumme auf 1.

Die update()-Methoden werden aus dem Interface implementiert.


Galileo Computing

12.8 Persistente Objekte und Serialisierung  downtop

Objekte liegen zwar immer nur zur Laufzeit vor, doch können sie in Java durch einen einfachen Mechanismus gesichert bzw. gelesen werden. Damit liegt ihre Struktur auch nach dem Beenden der virtuellen Maschine vor. Durch den Speichervorgang wird der Zustand und die Variablenbelegung zu einer bestimmten Zeit gesichert (persistent gemacht) und an anderer Stelle wieder hervorgeholt. Im Datenstrom sind alle Informationen, wie Objekttyp und Variablen, enthalten, 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 Netzwerke schwirren .

Die persistenten Objekte sichern also neben ihren eigenen Informationen auch die Unterobjekte – also die, die von der betrachtenden Stelle aus erreichbar sind. Beim Speichern wird rekursiv ein Objekt-Baum durchlaufen, um eine vollständige Datenstruktur zu erhalten. Der doppelte Zugriff auf ein Objekt wird dabei genauso beachtet wie der Fall, dass zyklische Abhängigkeiten auftreten können. Jedes Objekt bekommt dabei ein Handle, sodass es im Datenstrom nur einmal kodiert wird.


Galileo Computing

12.8.1 Objekte speichern  downtop

Ein Objekt zu sichern, ist sehr einfach, denn es gilt nur die writeObject()-Methode der Klasse aufzurufen. Der Übergabeparameter ist die Referenz auf das zu sichernde Objekt. writeObject() existiert als Funktion der Klasse ObjectOutputStream.

Beispiel Speichere einen String und das aktuelle Tagesdatum in der Datei datum.ser

Listing 12.33   SerializeAndDeserializeDate.java
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 ) { }
}

static void deserialize( String filename )
{
// …
}

public static void main( String args[] )
{
String filename = "c:/datum.ser";

serialize( filename );
deserialize( filename );
}
}

Ein ObjectOutputStream schreibt Objekte oder Primitive in einen OutputStream.

Wollen wir Objekte – oder allgemeiner Daten bzw. Primitive – serialisieren, so benötigen wir einen OutputStream. Da wir die Werte in eine Datei sichern wollen, eignet sich ein FileOutputStream am Besten (FileOutputStream erweitert die Klasse OutputStream). Der Dateiname wird meist so gewählt, dass er mit dem Präfix »ser« endet. Wir schaffen nun eine Verbindung zwischen der Datei und dem Objekt-Strom durch die Klasse ObjectOutputStream, die als Konstruktor einen OutputStream annimmt. ObjectOutputStream implementiert ObjectOutput, das ein Interface ist. So besitzt die Klasse ObjectOutput beispielsweise die Funktion writeObject() zum Schreiben von Objekten. Damit wird das Serialisieren vom String-Objekt (das »Today«) und dem anschließenden Datum-Objekt zum Kinderspiel.

class java.io.ObjectOutputStream
extends OutputStream implements ObjectOutput, ObjectStreamConstants

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

Das Interface ObjectOutput erweitert die Klasse DataOutput um das Schreiben von Objekten. Mit DataOutput können Primitive geschrieben werden und dieses Interface definiert die Methoden: 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). Nun erweitert ObjectOutput die Klasse DataOutput, um Methoden, Attribute, Strings und Objekte zu speichern. Natürlich können wir wegen der Vererbung in ObjectOutput wieder primitive Daten speichern. In der folgenden Aufzählung sind die Methoden aufgeführt. Allerdings finden sich unter den Funktionen keine, die Objekte vom Typ Class schreiben. Hier müssen ebenso Sonderbehandlungen getroffen werden wie bei Strings oder Arrays.

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 eine Array von Bytes.
gp  void write( byte b[], int off, int len ) throws IOException
Schreibt ein 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- bzw. Netzwerksystem auftreten.

Objekte übers 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-Klassen. Im oberen Beispiel benutzen 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 werden bzw. über das Netzwerk verschickt werden. Wie dies funktioniert zeigen die nächsten Zeilen:

Socket s = new Socket( "host", 
port );
OutputStream os = s.getOutputStream()
ObjectOutputStream oos = new ObjectOutputStream( os );
oos.writeObject( object );

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

Objekte in ein Bytefeld schreiben

Die Klasse 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 );
byte array[] = baos.toByteArray();

Nun steht das Objekt im Bytefeld und die Größe erfragen wir durch das Attribut length des Felds.


Galileo Computing

12.8.2 Objekte lesen  downtop

Die entgegengesetzte Richtung vom Schreiben ist das Lesen und dieses gestaltet sich ebenso einfach.

Beispiel Serialisierte Objekte aus einer Datei lesen Listing 12.34   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 ) { }
catch ( ClassNotFoundException e ) { }
}

Um an den Eingabestrom zu kommen, müssen wir ein InputStream verwenden. Da die Informationen aus einer Datei kommen, verwenden wir einen FileInputStream. Diesen verknüpfen wir mit einem ObjectInputStream, welcher die Daten aus in liest. Dann können wir aus dem ObjectInputStream den String und das Datum mit der Methode readObject() auslesen. Diese readObjekte()-Methode liest nun das Objekt und findet heraus, von welchem Typ es ist, und holt, wenn notwendig, auch noch Objekte, auf die verwiesen wird. Die explizite Typumwandlung kann natürlich bei einer falschen Zuweisung zu einem Fehler führen.

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. Die Klasse, die readObject() implementiert, muss natürlich wissen, wie es gelesen wird. ClassNotFoundException wird dann ausgelöst, wenn das Objekt zu einer Klasse gehört, die nicht gefunden werden kann.
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 Bytes in den Puffer b an der Stelle off genau len Bytes.
gp  long skip( long n ) throws IOException
Überspringt n Bytes im Eingabestrom. Die Anzahl der tatsächlich übersprungenen Zeichen werden 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.8.3 Die Schnittstelle Serializable  downtop

Bisher haben wir immer angenommen, dass eine Klasse weiß, wie sie geschrieben wird. Das funktioniert wie selbstverständlich bei allen vordefinierten Klassen, und so müssen wir uns bei writeObject(new Date()) keine Gedanken darüber machen, wie sich das Datum schreibt. Ist eine Klasse nicht in der Lage, sich zu serialisieren, wird eine NotSerializableException geworfen. Zu den Klassen, die sich nicht serialisieren lassen, gehören zum Beispiel Thread, Socket oder Klassen aus dem java.io-Paket.

Voraussetzung für die Serialisierung ist die Implementierung der Schnittstelle Serializable. Diese Schnittstelle enthält keine Methoden und ist nur eine Markierungsschnittstelle. Klassen, die diese Schnittstelle implementieren, signalisieren damit die Fähigkeit, sich selbst zu serialisieren.

Attribute einer Klasse automatisch schreiben

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

import java.io.Serializable;

class TestSer
implements Serializable
{
int a;
float f;
transient long l;
static u;
}

Schon jetzt lassen sich die Daten im Objekt speichern. Erzeugen wir ein TestSer-Objekt, nennen wir es ts, und rufen wir writeObject(ts) auf, so schiebt es all seine Variablen (hier a und f) in den Datenstrom. Ein spezielles Schlüsselwort transient markiert alle Attribute, die nicht persistent sein sollen. Daten, die später einfach rekonstruiert werden, müssen nicht abgespeichert werden, der Datenstrom ist somit kleiner. Alle Variablen, die mit static deklariert sind, werden ebenfalls nicht gesichert. Dies kann auch nicht sein, denn verschiedene Objekte teilen sich ja eine statische Variable. Wenn zwei Objekte wieder deserialisiert werden, könnte es sonst passieren, dass beide unterschiedliche Werte haben. Was sollte dann passieren?

Einen Vector serialisieren

Die meisten Java-Klassen lassen sich seit 1.1 serialisieren. Schauen wir in die Implementierung von java.util.Vector, so entdecken wir zunächst, dass Vector die Interfaces Clonable und Serializable implementiert.

public
class Vector implements Cloneable, java.io.Serializable
{
protected Object elementData[];
protected int elementCount;
protected int capacityIncrement;
private static final long serialVersionUID =
-2767605614048989439L;
...
}

Da keine der Variablen mit transient gekennzeichnet sind, schreiben sich alle Attribute, die die Klasse besitzt, in den Datenstrom.

Das Abspeichern selber in die Hand nehmen

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

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

und

private synchronized void
readObject( java.io.ObjectInputStream s )
throws IOException, ClassNotFoundException

Die Methode writeObject() ist für das Schreiben verantwortlich. Ist der Rumpf leer, so gelangen keine Informationen in den Strom und das Objekt wird folglich nicht gesichert. 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 ObjectOutput, ObjectStreamConstants

gp  public final void defaultWriteObject() throws IOException
Schreibt alle nicht statischen und nicht transienten Attribute in den Datenstrom. Die Methode wird automatisch beim Serialisieren aufgerufen, andernfalls erhalten wir eine NotActiveException.

Galileo Computing

12.8.4 Ian Wilmut10  und 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 ist, 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 Feldes ist lediglich ein zweites Feld, dessen Elemente auf die gleichen Vektoren zeigen. Eine Änderung wird also beiden Felder bewusst.

Möchten wir das Verhalten ändern und eine tiefe Kopie anfertigen, so haben wir mit einem kleinen Trick damit keine Mühe. Die Idee ist, dass wir das zu klonende Objekt einfach Serialisieren und dann wieder auspacken. Die zu klonenden Objekte müssen dann nur das Serializable-Interface implementieren.

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() machen müssen, ist das Objekt in einem Bytefeld zu serialisieren, es wieder auszulesen und zu einem Objekt konvertieren. Den Einsatz eines ByteArrayOutputStream haben wir schon gesehen, als wir die Länge eines Objekts herausfinden wollten. Nur fügen wir nun das Feld wieder einem ByteArrayInputStream zu, aus dessen Daten dann ObjectInputStream wieder das Objekt rekreieren kann.

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

public static void main( String 
args[] ) throws Exception
{
Map map = new HashMap() {{
put( "Cul de Paris",
"hinten unter dem Kleid getragenes Gestell oder Polster" );
}};

LinkedList l1 = new LinkedList();
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 );
}

Zunächst erstellen wir eine Map, die wir anschieß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 dem 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. Die Ausgabe ist dann:

[{}]
[{}]
[{Cul de Paris=hinten unter dem Kleid getragenes Gestell...}]

An diesem Beispiel sehen wir, wie wunderbar die Stream-Klassen zusammenarbeiten. Einzige Voraussetzung zum Gelingen ist die Implementierung der Schnittstelle Serializable. Da aber die zu klonenden Klassen 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.8.5 Felder sind implizit Serializable  downtop

Neben der Methode clone() und dem Attribut length besitzt ein Feld eine zweite wichtige Eigenschaft, die eng mit clone() verbunden ist: Ein Feld lässt sich serialisieren. Dazu muss aber ein Array-Objekt die Schnittstelle java.io.Serializable implementieren, und dies macht es auch versteckt.

Betrachten wir das folgende Programm, so erkennen wir, dass nur bei einer gültigen Referenz auf ein Feld-Objekt dieses Objekt instanceof Serializable ist.

Listing 12.35   ArrayIsSerializable.java
import java.io.Serializable;

class ArrayIsSerializable
{
public static void main( String args[] )
{
int f1[] = null;
int f2[] = new int[10];

Serializable s = (Serializable)f1;

System.out.println( s );

boolean b1 = f1 instanceof Serializable;
boolean b2 = f2 instanceof Serializable;

System.out.println( b1 );
System.out.println( b2 );
}
}

Galileo Computing

12.8.6 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 werden. Das bedeutet aber gleichzeitig, 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 noch 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 zu der Klasse passt, wird eine InvalidClassException ausgelöst. Das bedeutet, dass schon ein einfaches Zufügen von Attributen zu einem Fehler führt.

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

Listing 12.36   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.37   InvalidSer.java, Teil 2
import java.io.*;

public class InvalidSer
{
public static void main( String args[] ) throws Exception
{
das 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 ändern und zu 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

Aus dem oberen Fehlerauszug erkennen wir, dass der Serialisierungsmechanismus die SUID selber 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. Denn beim Einlesen gibt es Informationen, die nicht hinderlich sind. Wir sprechen in dem 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, so werden diese Attribute mit 0 oder null initialisiert.

Fehlende Felder

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

Die SUID lässt sich mit einem kleinen Dienstprogramm serialver berechnen. Dadurch erreichen wir eine stream-kompatible Serialisierung.

Dies wollen wir für unsere Klasse SerMe mit dem Dienstprogramm testen:

$ serialver SerMe
SerMe: static final long serialVersionUID = 9027745268614067035L;

Diese 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
{
int i;
double d;
static final long serialVersionUID = 9027745268614067035L;
}

Galileo Computing

12.8.7 Beispiele aus den Standard-Klassen  downtop

Nachfolgend wollen wir der Frage nachgehen, wie einige Objekte der Standard-Klassen serialisiert werden.

Hashtables implementieren write- und readObject()

Wir finden eine Implementierung der writeObject()-Methode beispielsweise in der Klasse java.util.Hashtable. In einer Hashtabelle befinden sich Werte nach einem bestimmten Schlüssel sortiert. Die Elemente können nicht einfach abgespeichert werden, da sich die Hashwerte beim Rekonstruieren geändert haben können. Daher durchläuft writeObject() das Feld mit den Elementen und sichert den Hashschlüssel und den Wert. Wir schauen uns einmal den Quellcode der Methoden an:

import java.io.ObjectOutputStream;
import java.io.ObjectInputStream;

class Hashtable extends Dictionary
implements Cloneable, java.io.Serializable
{
private transient HashtableEntry table[];
private transient int count;
...

private synchronized void
writeObject(java.io.ObjectOutputStream s)
throws IOException
{
// Schreibe length, threshold und loadfactor
s.defaultWriteObject();

// Schreibe Tabellenlänge und alle key- und value-Objekte
s.writeInt(table.length);
s.writeInt(count);

for (int index = table.length-1; index >= 0; index--)
{
HashtableEntry entry = table[index];

while (entry != null)
{
s.writeObject(entry.key);
s.writeObject(entry.value);
entry = entry.next;
}
}
}

private synchronized void
readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException
{
// Lies length, threshold and loadfactor
s.defaultReadObject();

// Lies Länge des Arrays und Anzahl der Elemente
int origlength = s.readInt();
int elements = s.readInt();

// Berechne die neue Größe, die etwa 5% über der alten Größe liegt
int length = (int)
(elements * loadFactor) + (elements / 20) + 3;
if (length > elements && (length & 1) == 0)
length--;

if (origlength > 0 && length > origlength)
length = origlength;

table = new HashtableEntry[length];
count = 0;

// Lies alle Elemente mit key und value
for (; elements > 0; elements--)
{
Object key = s.readObject();
Object value = s.readObject();
put(key, value);
}
}
}

Galileo Computing

12.8.8 Serialisieren in XML-Dateien  downtop

Der klassische Weg von einem Objekt zu einer persistenten Speicherung führt über den Serialisierungsmechanismus von Java über die Klassen ObjectOutputStream und Object InputStream. 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. Besonders wenn der Text strukturiert in einem XML-Format ist, finden wir mittlerweile viele Programme, die die Weiterverarbeitung sichern.

Für die Serialisierung in XML gibt es eine ganze Reihe von Bibliotheken. Wir wollen hier JSX von Macmillan vorstellen. In Java 1.4 ist Serialisierung in XML integriert, doch in Java 1.4 kann der Mechanismus nur Klassen serialisieren, die ausdrücklich Serializable implementieren, JSX ist das egal.


Galileo Computing

12.8.9 JSX (Java Serialization to XMLdowntop

Brendan Macmillan (bren@csse.monash.edu.au) implementierte die Java-Serialisieung in das Ausgabeformat XML in der Bibliothek JSX (Java Serialization to XML). Damit können Objekte in XML-Dokumente umgewandelt und eingelesen werden. Unter http://freshmeat.net/projects/jsx liegt die Bibliothek, die sehr einfach zu nutzen ist.

JSX erzeugt XML-Dateien, etwa der folgenden Art:

<java.util.Vector>
<java.lang.Integer valueOf="10"/>
<java.lang.String valueOf="Häufchen"/>
</java.util.Vector>

Der Mechanismus findet über Reflection die Attribute der Objekte heraus und schreibt sie nach obigem Schema in eine Datei. Die Tags der XML-Datei sind dann die Klassennamen.

Funktionsangebot

Der XML-Serialisierer schreibt das Objekt über ein ObjOut-Objekt und liest es über ObjIn ein. Ein ObjOut-Objekt kann über verschiedene Konstruktoren erzeugt werden. Die parametrisierten Konstruktoren nehmen einen PrintWriter, OutputStreamWriter oder OutputStream entgegen. Der Standard-Konstruktor schreibt die Ausgabe auf den Standard-Ausgabestrom. Für große Dateien ist es klug, hier einen gepufferten Strom zu nutzen. Intern nutzt die Klasse einen PrintWriter.

Beispiel Für gepufferte Ausgaben nutzen wir die Klasse BufferedWriter.

Um das Objekt o in eine Datei zu xml-serialisieren schreiben wir:

String file = "XYZ.xml";
ObjOut out = new ObjOut( new PrintWriter(
new BufferedWriter(
new FileWriter(file))) );
out.writeObject( o );

Zum deserialisieren nutzen wir de Klasse ObjIn und dann die Objektmethode readObject(). Das Objekt ObjIn lässt sich mit einem InputStreamReader und einem InputStream initialisieren. Der Standard-Konstruktor liest die Daten über den Standard-Eingabstrom ein.

Da ObjIn die Klasse ObjectInputStream erweitert, lassen sich über die Klasse auch alle anderen Datentypen lesen. Für ObjOut, die ObjectOutputStream erweitert, gilt gleiches.

Voraussetzungen und Einschränkungen

JSX stellt an die Objekte wenig Voraussetzungen. Der Mechanismus serialisiert alle primitiven Datentypen, auch geschachtelte Felder. Zyklische Abhängigkeiten werden genauso beachtet wie selbstimplementierte writeObject()- bzw. readObject()-Methoden. Die Klassen müssen keinen Konstruktor besitzen und nicht wie bei der Standard-Serialisierung die Schnittstelle Serializable implementieren.

JSX serialisiert die Daten ohne Verifizierung. Es wird keine DTD erstellt, die die Korrektheit sicherstellt. Die Erstellung einer DTD für die Dokumentation und Validierung ist jedoch schon implementiert und in der Testphase. Ebenso die Möglichkeit, über XSLT zyklische Verweise zu validieren. Bisher entspricht ein Tag immer dem Klassennamen und die Abbildung lässt sich nicht festlegen. Wünschenswert ist die Abbildung von Strukturen auf andere Klassen, sodass ein Austausch über Plattformen hinaus möglich ist. Serialisieren wir zum Beispiel ein ArrayList, so wünschen wir vielleicht, dass es zur Klasse Array wird. Beim Einlesen soll dann wieder Array zu ArrayList werden.

Nachfolgendes Programm serialisiert die Daten in einer Datei und liest sie anschließend wieder aus.

Listing 12.38   JSXDemo.java
import JSX.*;
import java.util.*;
import java.io.*;

public class JSXDemo
{
public static void main( String args[] ) throws Exception
{
String file = "c:/XYZ.xml";
ObjOut out = new ObjOut( new FileWriter(file) );

Integer i = new Integer(10);

Vector v = new Vector();
v.addElement( i );
v.addElement( "Häufchen" );
v.addElement( v );
out.writeObject( v );

out.writeObject( i );

class CircularRef {
CircularRef left, right;
}

CircularRef c = new CircularRef();
c.left = c.right = c;

out.writeObject( c );

out.writeObject(
new Object[] {
new int[] {2,3},
new int[] {5,7}
}
);

// out.close();

ObjIn in = new ObjIn( new FileReader(file) );

Object o = in.readObject();
System.out.println( o.getClass().getName() );
Integer io = (Integer)((Vector)o).get(0);

Object p = in.readObject();
System.out.println( p.getClass().getName() );

System.out.println( "Vektor-Int und Int sind " +
io == p ? "gleich." : "nicht gleich?" );

Object q = in.readObject();
System.out.println( q.getClass().getName() );

Object r = in.readObject();
System.out.println( r.getClass().getName() );

// in.close();
}
}

Die Ausgabe der letzten Zeilen ist Folgende:

java.util.Vector
java.lang.Integer
nicht gleich?
JSXDemo$1$CircularRef
[Ljava.lang.Object;

An dem Integer-Objekt können wir ablesen, das die aktuelle Implementierung hier noch einen Fehler hat.

Einen Blick auf die generierte Datei c:\XYZ.xml ergibt folgendes Bild:

<java.util.Vector>
<java.lang.Integer valueOf="10"/>
<java.lang.String valueOf="Häufchen"/>
<alias-ref alias="0"/>
</java.util.Vector>

<java.lang.Integer
value="10"/>

<JSXDemo$1$CircularRef>
<alias-ref obj-name="left" alias="0"/>
<alias-ref obj-name="right" alias="0"/>
</JSXDemo$1$CircularRef>

<ArrayOf-java.lang.Object length="2">
<ArrayOf-int length="2"
a0="2"
a1="3"/>
<ArrayOf-int length="2"
a0="5"
a1="7"/>
</ArrayOf-java.lang.Object>

Galileo Computing

12.8.10 XML-API von Sun  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

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.39   SerializeAndDeserializeXML.java
import java.io.*;
import java.util.*;
import java.beans.*;

public class SerializeAndDeserializeXML
{
static void serialize( String filename )
{
try
{
XMLEncoder o = new XMLEncoder(
new FileOutputStream(filename) );

o.writeObject( "Today" );
o.writeObject( new Date() );
o.close();
} catch ( IOException e ) { }
}

static void deserialize( String filename )
{
try
{
XMLDecoder o = new XMLDecoder(
new FileInputStream(filename) );

String string = (String) o.readObject();
Date date = (Date) o.readObject();
o.close();

System.out.println( string );
System.out.println( date );
}
catch ( IOException e ) { }
}

public static void main( String args[] )
{
String filename = "c:/datum.ser.xml";

serialize( filename );
deserialize( filename );
}
}

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.4.0-beta" class="java.beans.XMLDecoder">
<string>Today</string>
<object class="java.util.Date">
<long>997183601217</long>
</object>
</java>

Galileo Computing

12.9 Die Logging-API  toptop

Mit der Java   Logging-API kann eine Software-Meldung in eine XML- oder Text-Datei schreiben, die dann zur Wartung oder Kontrolle der Sicherheit eingesetzt werden kann. Zum Schreiben der Informationen dreht sich alles um ein Logger-Objekt, das etwa eine log()-Methode anbietet. An dieses Objekt wird ein Handler zugewiesen, der für das Schreiben der Daten verantwortlich ist. Ein FileHandler speichert etwa in eine Datei, ein ConsoleHandler gibt die Daten auf System.err aus.

Abbildung

Wir wollen die API benutzten und eine Exception erzwingen, die dann im catch-Block in eine Log-Datei geschrieben wird.

Listing 12.40   Logging .java
import java.util.logging.*;
import java.io.*;

public class Logging
{
private static Logger logger = Logger.getLogger("Logging");

public static void main( String args[] ) throws IOException
{
FileHandler fh = new FileHandler( "c:\\log.xml.txt" );

logger.addHandler( fh );

// logger.setLevel( Level.ALL );

logger.fine( "Dann mal los." );

try
{
((Object)null).toString();
}
catch ( Exception e )
{
logger.log( Level.WARNING, "oh oh", e );
}

logger.fine( "Ging alles glatt." );
}
}

Wenn wir die geschriebene Datei betrachten, dann finden wir sie in XML, sogar nach einer DTD formatiert. Die Typdefinition ist unter http://java.sun.com/j2se/1.4/docs/guide/util/logging/overview.html#3.0 definiert.

<?xml version="1.0" standalone="no"?>
<!DOCTYPE log SYSTEM "file:logger.dtd">
<log>
<record>
<date>2001-05-27T15:49:57</date>
<millis>990971397686</millis>
<sequence>0</sequence>
<logger>Logging</logger>
<level>WARNING</level>
<class>Logging</class>
<method>main</method>
<thread>10</thread>
<message>oh oh</message>
<exception>
<message>java.lang.NullPointerException</message>
<frame>
<class>Logging</class>
<method>main</method>
<line>20</line>
</frame>
</exception>
</record>

Damit das Datenvolumen kontrolliert und eingeschränkt werden kann, lassen sich die Logger oder Handler mit einem Filter versehen, die nur Ausschnitte aus dem Datenstrom schreiben. Und das Datenformat selbst kann zusätzlich mit Format-Objekten angepasst und lokalisiert werden, bevor sie in den Datenstrom geschrieben werden. Damit verschiedenen Detailgrade unterstützt werden, lässt sich über einen Level der Grad der Detaillierung festlegen. Sie reicht von FINEST bis SEVERE. Zur direkten Unterstützung der Levels wurden spezielle Methoden eingeführt. Eine weitere Anpassung von Logging besteht darin, es von außen über Property-Dateien zu verfeinern. Das Kapitel unter http://java.sun.com/j2se/1.4/docs/guide/util/logging/overview.html#1.8 verrät mehr davon.






1    EVA ist ein Akronym für »Eingabe, Verarbeitung, Ausgabe«. Diese Reihenfolge entspricht dem Arbeitsweg. Zunächst werden die Eingaben von einem Eingabegerät gelesen, dann durch den Computer verarbeitet und anschließend ausgegeben (in welcher Form auch immer).

2    Das englische Wort »file« geht auf das lateinische Wort filum zurück. Dies bezeichnete früher eine auf Draht aufgereite Sammlung von Schriftstücken.

3    Vor der Java SDK-Version 1.2 fehlte diese Möglichkeit. Dies machte sich bei Programmen zur Verzeichnisverwaltung, wie etwa einem Dateibrowser, nachteilig bemerkbar.

4    Obwohl die Funktionalität dokumentiert ist, findet sich unter der Bug-Nummer 4031440 kurz: »The main issue is that support for FilenameFilter in the FileDialog class was never implemented on any platform – it’s not that there’s a bug which needs to be fixed, but that there’s no code to run nor was the design ever evaluated to see if it *could* be implemented on our target platforms.«

5    Zurzeit noch nicht implementiert.

6    Mehr zu Grafikformaten unter http://www.oreilly.com/centers/gff/gff-faq/gff-faq3.htm.

7    Interessanterweise wurde danach der LZW-Algorithmus von der Sperry Company patentiert – dies zeigt eigentlich, wie unsinnig das Patentrecht in den USA ist.

8    Fletcher, J. G., »An Arithmetic Checksum for Serial Transmissions«. IEEE Transactions on Communications, Ausgabe. COM-30, Nummer. 1, Januar 1982, Seite 247-252.

9    Die Rede ist hier von RMI.

10    Ian Wilmut und sein Team haben im schottischen Roslin erstmals ein Schaf mit dem Namen Dolly geklont.

  

Java 2




Copyright © Galileo Press GmbH 2002
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, Gartenstraße 24, 53229 Bonn, fon: 0228.42150.0, fax 0228.42150.77, info@galileo-press.de