Titel   Inhalt   Suchen   Index   API  Go To Java 2, Zweite Auflage, Handbuch der Java-Programmierung
 <<    <     >    >>  Kapitel 18 - Character-Streams

18.2 Ausgabe-Streams



18.2.1 Die abstrakte Klasse Writer

Basis aller sequentiellen Ausgaben ist die abstrakte Klasse Writer des Pakets java.io, die eine Schnittstelle für stream-basierte Ausgaben zur Verfügung stellt:

protected Writer()

public void close()
public void flush()

public void write(int c)
public void write(char[] cbuf)
abstract public void write(char[] cbuf, int off, int len)
public void write(String str)
public void write(String str, int off, int len)
java.io.Writer

Neben einem parameterlosen Konstruktor definiert sie die Methoden close und flush und eine Reihe überladener write-Methoden. Die Bedeutung des Konstruktors liegt darin, den Ausgabestrom zu öffnen und für einen nachfolgenden Aufruf von write vorzubereiten, bevor er schließlich mit close wieder geschlossen wird. In der Grundform erwartet write einen einzigen int-Parameter und schreibt diesen als Byte in den Ausgabestrom. Daneben gibt es weitere Varianten, die ein Array von Bytes oder ein String-Objekt als Parameter erwarten und dieses durch wiederholten Aufruf der primitiven write-Methode ausgeben. Durch Aufruf von flush werden eventuell vorhandene Puffer geleert und die darin enthaltenen Daten an das Ausgabegerät weitergegeben.

Von abgeleiteten Klassen wird erwartet, daß sie mindestens die Methoden flush und close sowie write(char, int, int) überlagern. Aus Gründen der Performance ist es aber oftmals sinnvoll, auch die übrigen write-Methoden zu überlagern.

 Hinweis 

18.2.2 Auswahl des Ausgabegerätes

Als abstrakte Basisklasse kann Writer nicht instanziert werden. Statt dessen gibt es eine Reihe konkreter Klassen, die aus Writer abgeleitet wurden und deren Aufgabe es ist, die Verbindung zu einem konkreten Ausgabegerät herzustellen oder Filterfunktionen zu übernehmen. Tabelle 18.1 gibt eine Übersicht dieser abgeleiteten Klassen.

Klasse Bedeutung
OutputStreamWriter Abstrakte Basisklasse für alle Writer, die einen Character-Stream in einen Byte-Stream umwandeln
FileWriter Konkrete Ableitung von OutputStreamWriter zur Ausgabe in eine Datei
FilterWriter Abstrakte Basisklasse für die Konstruktion von Ausgabefiltern
PrintWriter Ausgabe aller Basistypen im Textformat
BufferedWriter Writer zur Ausgabepufferung
StringWriter Writer zur Ausgabe in einen String
CharArrayWriter Writer zur Ausgabe in ein Zeichen-Array
PipedWriter Writer zur Ausgabe in einen PipedReader

Tabelle 18.1: Aus Writer abgeleitete Klassen

In den folgenden Unterabschnitten werden die Klassen OutputStreamWriter, FileWriter, StringWriter und CharArrayWriter erläutert. Die Klassen BufferedWriter, PrintWriter und FilterWriter sind Thema des nächsten Abschnitts.

Die Klasse PipedWriter soll hier nicht erläutert werden. In Abschnitt 22.4.4 findet sich jedoch ein Beispiel zur Anwendung der Klassen PipedInputStream und PipedOutputStream, das auf PipedReader und PipedWriter übertragbar ist.

 Hinweis 

OutputStreamWriter und FileWriter

Die Klasse OutputStreamWriter ist die abstrakte Basisklasse für alle Writer, die eine Konvertierung zwischen Character- und Byte-Streams vornehmen. Sie enthält ein Objekt des Typs CharToByteConverter (aus dem undokumentierten Paket sun.io), das die Konvertierung der Ausgabezeichen bei allen schreibenden Zugriffen vornimmt. Als abstrakte Basisklasse ist OutputStreamWriter für uns allerdings nicht so interessant wie die daraus abgeleitete Klasse FileWriter, die die Ausgabe in eine Datei ermöglicht. Sie implementiert die abstrakten Eigenschaften von Writer und bietet vier zusätzliche Konstruktoren, die es erlauben, eine Datei zu öffnen:

public FileWriter(String fileName)
  throws IOException

public FileWriter(String fileName, boolean append)
  throws IOException

public FileWriter(File file)
  throws IOException

public FileWriter(FileDescriptor fd)
java.io.FileWriter

Am einfachsten kann eine Datei göffnet werden, indem der Dateiname als String-Parameter fileName übergeben wird. Falls fileName eine bereits vorhandene Datei bezeichnet, wird sie geöffnet und ihr bisheriger Inhalt gelöscht, andernfalls wird eine neue Datei mit diesem Namen angelegt. Sukzessive Aufrufe von write schreiben weitere Bytes in diese Datei. Wird zusätzlich der Parameter append mit dem Wert true an den Konstruktor übergeben, so werden die Ausgabezeichen an die Datei angehängt, falls sie bereits existiert.

Das folgende Programm erstellt eine Datei hallo.txt und schreibt die Zeile »Hallo JAVA« in die Datei:

001 /* Listing1801.java */
002 
003 import java.io.*;
004 
005 public class Listing1801
006 {
007   public static void main(String[] args)
008   {
009     String hello = "Hallo JAVA\r\n";
010     FileWriter f1;
011 
012     try {
013       f1 = new FileWriter("hallo.txt");
014       f1.write(hello);
015       f1.close();
016     } catch (IOException e) {
017       System.out.println("Fehler beim Erstellen der Datei");
018     }
019   }
020 }
Listing1801.java
Listing 18.1: Erstellen einer Datei

Fast alle Methoden der Klassen OutputStreamWriter und FileWriter deklarieren die Ausnahme IOException, die als allgemeine Fehleranzeige im Paket java.io verwendet wird. IOException kann von einer Vielzahl von Methoden ausgelöst werden und hat dabei teilweise sehr unterschiedliche Bedeutungen. So bedeutet sie beim Aufruf des Konstruktors, daß es nicht möglich war, die Datei anzulegen, was wiederum eine ganze Reihe von Gründen haben kann. Beim Aufruf von write signalisiert sie einen Schreibfehler, und bei close zeigt sie einen nicht näher spezifizierten I/O-Fehler an.

In unserem Beispiel wurden alle Ausnahmen dieses Typs von einer einzigen try-catch-Anweisung behandelt. Falls eine differenziertere Fehlererkennung nötig ist, macht es Sinn, für die unterschiedlichen I/O-Operationen verschiedene try-catch-Anweisungen vorzusehen.

Die anderen Konstruktoren von FileWriter erwarten ein File-Objekt bzw. ein Objekt vom Typ FileDescriptor. Während wir auf die Klasse FileDescriptor nicht näher eingehen werden, ist ein File die Repräsentation einer Datei im Kontext ihres Verzeichnisses. Wir werden später in Kapitel 21 darauf zurückkommen.

 Hinweis 

StringWriter und CharArrayWriter

Die Klassen StringWriter und CharArrayWriter sind ebenfalls aus Writer abgeleitet. Im Gegensatz zu FileWriter schreiben sie ihre Ausgabe jedoch nicht in eine Datei, sondern in einen dynamisch wachsenden StringBuffer bzw. in ein Zeichenarray.

StringWriter besitzt zwei Konstruktoren:

public StringWriter()

public StringWriter(int initialSize)
java.io.StringWriter

Der parameterlose Konstruktor legt einen StringWriter mit der Standardgröße eines StringBuffer-Objekts an, während der zweite Konstruktor es erlaubt, die initiale Größe selbst festzulegen. Wie schon erwähnt, wächst der interne Puffer automatisch, wenn fortgesetzte Aufrufe von write dies erforderlich machen. Die schreibenden Zugriffe auf den Puffer erfolgen mit den von Writer bekannten write-Methoden.

Für den Zugriff auf den Inhalt des Puffers stehen die Methoden getBuffer und toString zur Verfügung:

public StringBuffer getBuffer()

public String toString()
java.io.StringWriter

Die Methode getBuffer liefert den internen StringBuffer, während toString den Puffer in einen String kopiert und diesen an den Aufrufer liefert.

Die Klasse CharArrayWriter arbeitet ähnlich wie StringWriter, schreibt die Zeichen allerdings in ein Character-Array. Analog zu StringWriter wächst dieses automatisch bei fortgesetzten Aufrufen von write. Die Konstruktoren haben dieselbe Struktur wie bei StringWriter, und auch die dort verfügbaren write-Methoden stehen allesamt hier zur Verfügung:

public CharArrayWriter()

public CharArrayWriter(int initialSize)
java.io.CharArrayWriter

Auf den aktuellen Inhalt des Arrays kann mit Hilfe der Methoden toString und toCharArray zugegriffen werden:

public String toString()

public char[] toCharArray()
java.io.CharArrayWriter

Zusätzlich stehen die Methoden reset und size zur Verfügung, mit denen der interne Puffer geleert bzw. die aktuelle Größe des Zeichen-Arrays ermittelt werden kann. Durch Aufruf von writeTo kann des weiteren der komplette Inhalt des Arrays an einen anderen Writer übergeben und so beispielsweise mit einem einzigen Funktionsaufruf in eine Datei geschrieben werden:

public void reset()

public int size()

public void writeTo(Writer out)
  throws IOException
java.io.CharArrayWriter

18.2.3 Schachteln von Ausgabe-Streams

Wie eingangs erwähnt, ist es in Java möglich, Streams zu schachteln. Dazu gibt es die aus Writer abgeleiteten Klassen BufferedWriter, PrintWriter und FilterWriter, deren wesentlicher Unterschied zu Writer darin besteht, daß sie als Membervariable einen zusätzlichen Writer besitzen, der im Konstruktor übergeben wird. Wenn nun die write-Methode eines derart geschachtelten Writers aufgerufen wird, gibt sie die Daten nicht direkt an den internen Writer weiter, sondern führt zuvor erst die erforderlichen Filterfunktionen aus. Damit lassen sich beliebige Filterfunktionen transparent realisieren.

BufferedWriter

Diese Klasse hat die Aufgabe, Stream-Ausgaben zu puffern. Dazu enthält sie einen internen Puffer, in dem die Ausgaben von write zwischengespeichert werden. Erst wenn der Puffer voll ist oder die Methode flush aufgerufen wird, werden alle gepufferten Ausgaben in den echten Stream geschrieben. Das Puffern der Ausgabe ist immer dann nützlich, wenn die Ausgabe in eine Datei geschrieben werden soll. Durch die Verringerung der write-Aufrufe reduziert sich die Anzahl der Zugriffe auf das externe Gerät, und die Performance wird erhöht.

BufferedWriter besitzt zwei Konstruktoren:

public BufferedWriter(Writer out)

public BufferedWriter(Writer out, int size)
java.io.BufferedWriter

In beiden Fällen wird ein bereits existierender Writer übergeben, an den die gepufferten Ausgaben weitergereicht werden. Falls die Größe des Puffers nicht explizit angegeben wird, legt BufferedWriter einen Standardpuffer an, dessen Größe für die meisten Zwecke ausreichend ist. Die write-Methoden von BufferedWriter entsprechen denen der Klasse Writer.

Zusätzlich gibt es eine Methode newLine, mit der eine Zeilenschaltung in den Stream geschrieben werden kann. Diese wird dem System-Property line.separator entnommen und entspricht damit den lokalen Konventionen.

Das nachfolgende Beispiel demonstriert die gepufferte Ausgabe einer Reihe von Textzeilen in eine Datei buffer.txt:

001 /* Listing1802.java */
002 
003 import java.io.*;
004 
005 public class Listing1802
006 {
007   public static void main(String[] args)
008   {
009      Writer f1;
010      BufferedWriter f2;
011      String s;
012 
013      try {
014        f1 = new FileWriter("buffer.txt");
015        f2 = new BufferedWriter(f1);
016        for (int i = 1; i <= 10000; ++i) {
017          s = "Dies ist die " + i + ". Zeile";
018          f2.write(s);
019          f2.newLine();
020        }
021        f2.close();
022        f1.close();
023     } catch (IOException e) {
024       System.out.println("Fehler beim Erstellen der Datei");
025     }
026   }
027 }
Listing1802.java
Listing 18.2: Gepufferte Ausgabe in eine Datei

Dieses Beispiel erzeugt zunächst einen neuen Writer f1, um die Datei buffer.txt anzulegen. Dieser wird anschließend als Parameter an den BufferedWriter f2 übergeben, der dann für den Aufruf der Ausgaberoutinen verwendet wird.

Die etwas umständliche Verwendung zweier Stream-Variablen läßt sich vereinfachen, indem die Konstruktoren direkt beim Aufruf geschachtelt werden. Das ist die unter Java übliche Methode, die auch zukünftig bevorzugt verwendet werden soll:

 Tip 

001 /* Listing1803.java */
002 
003 import java.io.*;
004 
005 public class Listing1803
006 {
007   public static void main(String[] args)
008   {
009     BufferedWriter f;
010     String s;
011 
012     try {
013       f = new BufferedWriter(
014          new FileWriter("buffer.txt"));
015       for (int i = 1; i <= 10000; ++i) {
016         s = "Dies ist die " + i + ". Zeile";
017         f.write(s);
018         f.newLine();
019       }
020       f.close();
021     } catch (IOException e) {
022       System.out.println("Fehler beim Erstellen der Datei");
023     }
024   }
025 }
Listing1803.java
Listing 18.3: Schachteln von Writer-Konstruktoren

PrintWriter

Die stream-basierten Ausgaberoutinen in anderen Programmiersprachen bieten meistens die Möglichkeit, alle primitiven Datentypen in textueller Form auszugeben. Java realisiert dieses Konzept über die Klasse PrintWriter, die Ausgabemethoden für alle primitiven Datentypen und für Objekttypen zur Verfügung stellt. PrintWriter besitzt folgende Konstruktoren:

public PrintWriter(Writer out)

public PrintWriter(Writer out, boolean autoflush)
java.io.PrintWriter

Der erste Konstruktor instanziert ein PrintWriter-Objekt durch Übergabe eines Writer-Objekts, auf das die Ausgabe umgeleitet werden soll. Beim zweiten Konstruktor gibt zusätzlich der Parameter autoflush an, ob nach der Ausgabe einer Zeilenschaltung automatisch die Methode flush aufgerufen werden soll.

Die Ausgabe von primitiven Datentypen wird durch eine Reihe überladener Methoden mit dem Namen print realisiert. Zusätzlich gibt es alle Methoden in einer Variante println, bei der automatisch an das Ende der Ausgabe eine Zeilenschaltung angehängt wird. println existiert darüber hinaus parameterlos, um lediglich eine einzelne Zeilenschaltung auszugeben. Damit stehen folgende print-Methoden zur Verfügung (analog für println):

public void print(boolean b)
public void print(char c)
public void print(char[] s)
public void print(double d)
public void print(float f)
public void print(int i)
public void print(long l)
public void print(Object obj)
public void print(String s)
java.io.PrintWriter

Das folgende Beispiel berechnet die Zahlenfolge 1 + 1/2 + 1/4 + ... und gibt die Folge der Summanden und die aktuelle Summe unter Verwendung mehrerer unterschiedlicher print-Routinen in die Datei zwei.txt aus:

001 /* Listing1804.java */
002 
003 import java.io.*;
004 
005 public class Listing1804
006 {
007   public static void main(String[] args)
008   {
009     PrintWriter f;
010     double sum = 0.0;
011     int nenner;
012 
013     try {
014       f = new PrintWriter(
015           new BufferedWriter(
016           new FileWriter("zwei.txt")));
017 
018       for (nenner = 1; nenner <= 1024; nenner *= 2) {
019         sum += 1.0 / nenner;
020         f.print("Summand: 1/");
021         f.print(nenner);
022         f.print("   Summe: ");
023         f.println(sum);
024       }
025       f.close();
026     } catch (IOException e) {
027       System.out.println("Fehler beim Erstellen der Datei");
028     }
029   }
030 }
Listing1804.java
Listing 18.4: Die Klasse PrintWriter

Das Programm verwendet Methoden zur Ausgabe von Strings, Ganz- und Fließkommazahlen. Nach Ende des Programms hat die Datei zwei.txt folgenden Inhalt:

Summand: 1/1   Summe: 1
Summand: 1/2   Summe: 1.5
Summand: 1/4   Summe: 1.75
Summand: 1/8   Summe: 1.875
Summand: 1/16   Summe: 1.9375
Summand: 1/32   Summe: 1.96875
Summand: 1/64   Summe: 1.98438
Summand: 1/128   Summe: 1.99219
Summand: 1/256   Summe: 1.99609
Summand: 1/512   Summe: 1.99805
Summand: 1/1024   Summe: 1.99902

Das vorliegende Beispiel realisiert sogar eine doppelte Schachtelung von Writer-Objekten. Das PrintWriter-Objekt schreibt in einen BufferedWriter, der seinerseits in den FileWriter schreibt. Auf diese Weise werden Datentypen im ASCII-Format gepuffert in eine Textdatei geschrieben. Eine solche Schachtelung ist durchaus üblich in Java und kann in einer beliebigen Tiefe ausgeführt werden.

 Tip 

FilterWriter

Wie bereits mehrfach angedeutet, bietet die Architektur der Writer-Klassen die Möglichkeit, eigene Filter zu konstruieren. Dies kann beispielsweise durch Überlagern der Klasse Writer geschehen, so wie es etwa bei PrintWriter oder BufferedWriter realisiert wurde. Der offizielle Weg besteht allerdings darin, die abstrakte Klasse FilterWriter zu überlagern. FilterWriter besitzt ein internes Writer-Objekt out, das bei der Instanzierung an den Konstruktor übergeben wird. Zusätzlich überlagert es drei der vier write-Methoden, um die Ausgabe auf out umzuleiten. Die vierte write-Methode (write(String)) wird dagegen nicht überlagert, sondern ruft gemäß ihrer Implementierung in Writer die Variante write(String, int, int) auf.

Soll eine eigene Filterklasse konstruiert werden, so ist wie folgt vorzugehen:

Anschließend kann die neue Filterklasse wie gewohnt in einer Kette von geschachtelten Writer-Objekten verwendet werden.

Wir wollen uns die Konstruktion einer Filterklasse anhand der folgenden Beispielklasse UpCaseWriter ansehen, deren Aufgabe es ist, innerhalb eines Streams alle Zeichen in Großschrift zu konvertieren:

001 /* Listing1805.java */
002 
003 import java.io.*;
004 
005 class UpCaseWriter
006 extends FilterWriter
007 {
008   public UpCaseWriter(Writer out) 
009   {
010     super(out);
011   }
012 
013   public void write(int c)
014   throws IOException
015   {
016     super.write(Character.toUpperCase((char)c));
017   }
018 
019   public void write(char[] cbuf, int off, int len)
020   throws IOException
021   {
022     for (int i = 0; i < len; ++i) {
023       write(cbuf[off + i]);
024     }
025   }
026 
027   public void write(String str, int off, int len)
028   throws IOException
029   {
030     write(str.toCharArray(), off, len);
031    }
032 }
033 
034 public class Listing1805
035 {
036    public static void main(String[] args)
037    {
038       PrintWriter f;
039       String s = "und dieser String auch";
040 
041       try {
042          f = new PrintWriter(
043              new UpCaseWriter(
044              new FileWriter("upcase.txt")));
045          //Aufruf von außen
046          f.println("Diese Zeile wird schön groß geschrieben");
047          //Test von write(int)
048          f.write('a');
049          f.println();
050          //Test von write(String)
051          f.write(s);
052          f.println();
053          //Test von write(String, int, int)
054          f.write(s,0,17);
055          f.println();
056          //Test von write(char[], int, int)
057          f.write(s.toCharArray(),0,10);
058          f.println();
059          //---
060          f.close();
061       } catch (IOException e) {
062          System.out.println("Fehler beim Erstellen der Datei");
063       }
064    }
065 }
Listing1805.java
Listing 18.5: Konstruktion einer eigenen FilterWriter-Klasse

Im Konstruktor wird lediglich der Superklassen-Konstruktor aufgerufen, da keine weiteren Aufgaben zu erledigen sind. Die drei write-Methoden werden so überlagert, daß jeweils zunächst die Ausgabezeichen in Großschrift konvertiert werden und anschließend die passende Superklassenmethode aufgerufen wird, um die Daten an den internen Writer out zu übergeben.

Der Test von UpCaseWriter erfolgt mit der Klasse Listing1805. Sie schachtelt einen UpCaseWriter innerhalb eines FileWriter- und eines PrintWriter-Objekts. Die verschiedenen Aufrufe der write- und println-Methoden rufen dann jede der vier unterschiedlichen write-Methoden des UpCaseWriter-Objekts mindestens einmal auf, um zu testen, ob die Konvertierung in jedem Fall vorgenommen wird. Die Ausgabe des Programms ist:

DIESE ZEILE WIRD SCHÖN GROß GESCHRIEBEN
A
UND DIESER STRING AUCH
UND DIESER STRING
UND DIESER

Wenn man sich die Implementierung der write-Methoden von UpCaseWriter genauer ansieht, könnte man auf die Idee kommen, daß sie wesentlich performanter implementiert werden könnten, wenn nicht alle Ausgaben einzeln an write(int) gesendet würden. So scheint es nahezuliegen, beispielsweise write(String, int, int) in der folgenden Form zu implementieren, denn die Methode toUpperCase existiert auch in der Klasse String:

 Warnung 

super.write(str.toUpperCase(), off, len);

Die Sache hat nur leider den Haken, daß String.toUpperCase möglicherweise die Länge des übergebenen Strings verändert. So wird beispielsweise das "ß" in ein "SS" umgewandelt (an sich lobenswert!) und durch die unveränderlichen Konstanten off und len geht ein Zeichen verloren. Der hier beschrittene Workaround besteht darin, grundsätzlich die Methode Character.toUpperCase zu verwenden. Sie kann immer nur ein einzelnes Zeichen zurückgeben und läßt damit beispielsweise ein "ß" bei Umwandlung unangetastet.

Als interessante Erkenntnis am Rande lernen wir dabei, daß offensichtlich String.toUpperCase und Character.toUpperCase unterschiedlich implementiert sind. Ein Blick in den Quellcode der Klasse String bestätigt den Verdacht und zeigt zwei Sonderbehandlungen, eine davon für das "ß", wie folgender Auszug aus dem Quellcode des JDK 1.1 belegt (im JDK 1.2 sieht es ähnlich aus):

for (i = 0; i < len; ++i) {
   char ch = value[offset+i];
   if (ch == '\u00DF') { // sharp s
      result.append("SS");
      continue;
   }
   result.append(Character.toUpperCase(ch));
}

Im Umfeld dieser Methode sollte man also mit der nötigen Umsicht agieren. Das Gegenstück lowerCase hat diese Besonderheit übrigens nicht.


 Titel   Inhalt   Suchen   Index   API  Go To Java 2, Zweite Auflage, Addison Wesley, Version 2.0
 <<    <     >    >>  © 2000 Guido Krüger, http://www.gkrueger.com