Titel | Inhalt | Suchen | Index | API | Go To Java 2, Zweite Auflage, Handbuch der Java-Programmierung |
<< | < | > | >> | Kapitel 18 - Character-Streams |
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. |
|
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. |
|
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 |
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. |
|
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 |
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.
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 |
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: |
|
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 |
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 |
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. |
|
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 |
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: |
|
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 |