19.4 Basisklassen für die Ein-/Ausgabe
Unterschiedliche Klassen zum Lesen und Schreiben von Binär- und Zeichendaten sammeln sich im Paket java.io. Für die byteorientierte Verarbeitung, etwa von PDF- oder MP3-Dateien, gibt es andere Klassen als für Textdokumente, zum Beispiel HTML-Dokumente oder Konfigurationsdateien. Binär- von Zeichendaten zu trennen ist sinnvoll, da zum Beispiel beim Einlesen von Textdateien diese immer in Unicode konvertiert werden müssen, da Java intern alle Zeichen in Unicode kodiert.
Die Stromklassen aus dem java.io-Paket sind um drei zentrale Prinzipien aufgebaut:
Es gibt abstrakte Basisklassen, die Operationen für die Ein-/Ausgabe vorschreiben.
Die abstrakten Basisklassen gibt es einmal für Unicode-Zeichen und einmal für Bytes.
Die Implementierungen der abstrakten Basisklassen realisieren entweder die konkrete Eingabe in eine bestimmte Ressource bzw. die Ausgabe aus einer bestimmten Ressource (etwa einer Datei oder einem Speicherbereich) oder sind Filter.
19.4.1 Die vier abstrakten Basisklassen
Da Klassen zum Lesen und Schreiben von Unicode-Zeichen und Bytes getrennt werden müssen, gibt es folglich Klassen zur Ein-/Ausgabe von Bytes (oder Byte-Arrays) und Klassen zur Ein-/Ausgabe von Unicode-Zeichen (Arrays oder Strings). Die Klassen lauten:
Bytes | Zeichen | |
---|---|---|
Eingabe | InputStream | Reader |
Ausgabe | OutputStream | Writer |
Die vier Typen wollen wir Stromklassen nennen. In diesen Klassen sind die zu erwartenden Methoden wie read(…) und write(…) zu finden.
Die Klassen InputStream und OutputStream bilden die Basisklassen für alle byteorientierten Klassen und dienen somit als Bindeglied bei Methoden, 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. Das Gleiche gilt für Reader und Writer; sie sind die abstrakten Basisklassen zum Lesen und Schreiben von Unicode-Zeichen und Unicode-Zeichenfolgen. Die Basisklassen geben abstrakte read(…)- oder write(…)-Methoden vor, und die Unterklassen überschreiben die Methoden, da nur sie wissen, wie etwas tatsächlich gelesen oder geschrieben wird.
19.4.2 Die abstrakte Basisklasse OutputStream
Wenn wir uns den OutputStream anschauen, dann sehen wir auf den ersten Blick, dass hier alle wesentlichen Operationen rund um das Schreiben versammelt sind. Der Clou bei allen Datenströmen ist, dass spezielle Unterklassen wissen, wie sie genau die vorgeschriebene Funktionalität implementieren. Das heißt, dass ein konkreter Datenstrom, der in Dateien oder in eine Netzwerkverbindung schreibt, weiß, wie Bytes in Dateien oder ins Netzwerk kommen. Und an der Stelle ist Java mit seiner Plattformunabhängigkeit am Ende, denn auf einer so tiefen Ebene können nur native Methoden die Bytes schreiben.
abstract class java.io.OutputStream
implements Closeable, Flushable
abstract void write(int b) throws IOException
Schreibt ein einzelnes Byte in den Datenstrom.void write(byte[] b) throws IOException
Schreibt die Bytes aus dem Array in den Strom.void write(byte[] b, int off, int len) throws IOException
Schreibt Teile des Byte-Arrays, nämlich len Byte ab der Position off, in den Ausgabestrom.void close() throws IOException
Schließt den Datenstrom. Dies ist die einzige Methode aus Closeable.void flush() throws IOException
Schreibt noch im Puffer gehaltene Daten. Dies ist die einzige Methode aus der Schnittstelle Flushable.static OutputStream nullOutputStream()
Liefert einen OutputStream, der alle Bytes verwirft. Neu in Java 11.
Die IOException ist keine RuntimeException und muss behandelt werden.
[zB] Beispiel
Die Klasse ByteArrayOutputStream ist eine Unterklasse von OutputStream und speichert alle Daten in einem internen byte-Array. Schreiben wir ein paar Daten mit den drei gegebenen Methoden hinein:
byte[] bytes = { 'O', 'N', 'A', 'L', 'D' };
// 0 1 2 3 4
ByteArrayOutputStream out = new ByteArrayOutputStream();
try {
out.write( 'D' ); // schreibe D
out.write( bytes ); // schreibe ONALD
out.write( bytes, 1, 2 ); // schreibe NA
System.out.println( out.toString( StandardCharsets.ISO_8859_1.name() ) );
}
catch ( IOException e ) {
e.printStackTrace();
}
Über konkrete und abstrakte Methoden *
Zwei Eigenschaften lassen sich an den Methoden vom OutputStream ablesen: zum einen, dass nur Bytes geschrieben werden, und zum anderen, dass nicht wirklich alle Methoden abstract sind und von Unterklassen für konkrete Ausgabeströme überschrieben werden müssen. Nur write(int) ist abstrakt und elementar. Das ist trickreich, denn tatsächlich lassen sich die Methoden, die ein Byte-Array schreiben, auf die Methode abbilden, die ein einzelnes Byte schreibt. Werfen wir einen Blick in den Quellcode der Bibliothek:
public abstract void write(int b) throws IOException;
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 die Arbeit sehr bequem an andere Methoden verschieben. Doch diese Implementierung ist nicht optimal! 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 große Byte-Arrays schreibt, etwa eine 5-MiB-Datei, die im Speicher steht. Dann werden für jedes Byte im Byte-Array in einer Schleife alle Bytes der Reihe nach an eine vermutlich native Methode übergeben. Wenn es so implementiert wäre, könnten wir die Geschwindigkeit des Mediums überhaupt nicht nutzen, zumal jedes Dateisystem Funktionen bereitstellt, mit denen sich ganze Blöcke übertragen lassen. Glücklicherweise sieht die Implementierung nicht so aus, da wir in dem Modell vergessen haben, dass die Unterklasse zwar die abstrakte Methode implementieren muss, aber immer noch andere Methoden überschreiben kann. Ein Blick auf die Unterklasse FileOutputStream bestätigt dies.
[»] Hinweis
Ruft eine Oberklasse eine abstrakte Methode auf, die später die Unterklasse implementiert, ist das ein Entwurfsmuster mit dem Namen Schablonenmuster oder auf Englisch template pattern.
Gleichzeitig stellt sich die Frage, wie ein OutputStream, der die Eigenschaften für alle erdenklichen Ausgabeströme vorschreibt, wissen kann, wie ein spezieller Ausgabestrom etwa mit close() geschlossen wird oder seine gepufferten Bytes mit flush() schreibt – die Methoden müssten doch auch abstrakt sein! Das weiß OutputStream natürlich nicht, aber die Entwickler haben sich dazu entschlossen, eine leere Implementierung anzugeben. Der Vorteil besteht darin, dass Unterklassen nicht verpflichtet werden, die Methoden immer sinnvoll zu überschreiben.
19.4.3 Die abstrakte Basisklasse InputStream
Das Gegenstück zu OutputStream ist InputStream; jeder binäre Eingabestrom wird durch die abstrakte Klasse InputStream repräsentiert. Die Konsoleneingabe System.in ist vom Typ InputStream. Die Klasse bietet mehrere readXXX(…)-Methoden und ist auch ein wenig komplexer als OutputStream.
abstract class java.io.InputStream
implements Closeable
int available() throws IOException
Gibt die Anzahl der verfügbaren Zeichen im Datenstrom zurück, die sofort ohne Blockierung gelesen werden können.abstract int read() throws IOException
Liest ein Byte aus dem Datenstrom und liefert ihn zurück. Die Rückgabe ist –1, wenn der Datenstrom keine Daten mehr liefert. Falls Daten grundsätzlich noch verfügbar sind, blockiert die Methode. Der Rückgabetyp ist int, weil –1 (0xFFFFFFFF) das Ende des Datenstroms anzeigt und ein –1 als byte (das wäre 0xFF) nicht von einem normalen Datum unterschieden werden könnte. Schade, dass es für –1 in der Java-API keine Konstante gibt.[ 266 ](Es könnte TT_EOF aus der Klasse java.io.StreamTokenizer genutzt werden. Oracle selbst wollte keine Konstante spendieren und hat die Anfrage schnell geschlossen: https://bugs.java.com/bugdatabase/view_bug.do?bug_id=1204354. )int read(byte[] b) throws IOException
Liest bis zu b.length Bytes aus dem Datenstrom und setzt sie in das Array b. Die tatsächliche Länge der gelesenen Bytes wird zurückgegeben und muss nicht b.length sein; es können auch weniger Bytes gelesen werden. In der Basisklasse InputStream ist das einfach als return read(b, 0, b.length); implementiert.int read(byte[] b, int off, int len) throws IOException
Liest den Datenstrom aus und setzt die Daten in das Byte-Array b, an der Stelle off beginnend. Zudem begrenzt len die maximale Anzahl der zu lesenden Bytes. Intern ruft die Methode zunächst read() auf, und wenn es zu einer Ausnahme kommt, endet damit auch unsere Methode mit einer Ausnahme. Es folgen wiederholte Aufrufe von read(), die dann enden, wenn read() die Rückgabe –1 liefert. Falls es zu einer Ausnahme kommt, wird diese aufgefangen und nicht gemeldet.int readNBytes(byte[] b, int off, int len) throws IOException
Versucht len viele Bytes aus dem Datenstrom zu lesen und in das Byte-Array zu setzen. Im Gegensatz zu read(byte[], int, int) übernimmt readNBytes(…) mehrere Anläufe, len viele Daten zu beziehen. Dabei greift es auf read(byte[], int, int) zurück.byte[] readNBytes(int len) throws IOException
Versucht len viele Bytes aus dem Strom zu lesen und in das Rückgabe-Array zu setzen. Das Array kann kleiner als len sein, wenn vorher das Ende des Strom erreicht wurde. Seit Java 9.byte[] readAllBytes() throws IOException
Liest alle verbleibenden Daten aus dem Datenstrom und liefert ein Array mit diesen Bytes als Rückgabe. Seit Java 9.long transferTo(OutputStream out) throws IOException
Liest alle Bytes aus dem Datenstrom aus und schreibt sie in out. Wenn es zu einer Ausnahme kommt, wird empfohlen, beide Datenströme zu schließen. Seit Java 9.long skip(long n) throws IOException
Überspringt eine Anzahl von Zeichen. Die Rückgabe gibt die tatsächlich gesprungenen Bytes zurück, was nicht mit n identisch sein muss.boolean markSupported()
Gibt einen Wahrheitswert zurück, der besagt, 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.void mark(int readlimit)
Merkt sich die aktuelle Position im Datenstrom. Ist markSupported() gleich true, können danach mindestes readlimit Bytes gelesen und kann wieder zu der gemerkten Stelle zurückgesprungen werden.void reset() throws IOException
Springt wieder zu der Position zurück, die mit mark() gesetzt wurde.void close() throws IOException
Schließt den Datenstrom. Operation aus der Schnittstelle Closeable.static InputStream nullInputStream()
Liefert einen InputStream, der keine Bytes liest.
Auffällig ist, dass bis auf mark(int) und markSupported() alle Methoden im Fehlerfall eine IOException auslösen. Bei der read(…)-Methode, die mehrere Bytes liest, wird immer ein Array als Puffer übergeben, in den der InputStream hineinschreibt; das gleiche Prinzip finden wir aber auch beim Reader. Es gibt keine Methode, die als Rückgabe ein Array mit den gelesenen Daten liefert. Das hat zwei Gründe: Zum einen werden Puffer in der Regel wiederverwendet und wir hätten so unter Umständen sehr viele Objekte, und zum anderen könnte der Datenstrom sehr groß oder sogar unendlich sein.
[»] Hinweis
available() liefert die Anzahl der Bytes, die ohne Blockierung gelesen werden können. (»Blockieren« bedeutet, dass die Methode nicht sofort zurückkehrt, sondern erst wartet, bis neue Daten vorhanden sind.) Die Rückgabe von available() sagt nichts darüber aus, wie viele Zeichen der InputStream insgesamt hergibt. Während aber bei FileInputStream die Methode available() üblicherweise doch die Dateilänge liefert, ist dies bei den Netzwerk-Streams im Allgemeinen nicht der Fall.
19.4.4 Die abstrakte Basisklasse Writer
Die abstrakte Klasse Writer ist der Basistyp für alle zeichenbasierten schreibenden Klassen.
abstract class java.io.Writer
implements Appendable, Closeable, Flushable
void write(int c) throws IOException
Schreibt ein einzelnes Unicode-Zeichen. Nur der niedrige Teil (16 Bit des int) der 32-Bit-Ganzzahl wird geschrieben.void write(char[] cbuf) throws IOException
Schreibt ein Array von Zeichen.abstract void write(char[] cbuf, int off, int len) throws IOException
Schreibt len Zeichen des Arrays cbuf ab der Position off.void write(String str) throws IOException
Schreibt den String str.void write(String str, int off, int len) throws IOException
Schreibt len Zeichen der Zeichenkette str ab der Position off.Writer append(char c) throws IOException
Hängt ein Zeichen an. Verhält sich wie write(c), nur liefert es, wie die Schnittstelle Appendable verlangt, ein Appendable zurück. Writer ist ein passendes Appendable.Writer append(CharSequence csq) throws IOException
Hängt eine Zeichenfolge an. Implementierung aus der Schnittstelle Appendable.Writer append(CharSequence csq, int start, int end) throws IOException
Hängt Teile einer Zeichenfolge an. Implementierung aus der Schnittstelle Appendable.abstract void flush() throws IOException
Schreibt den internen Puffer. Hängt verschiedene flush()-Aufrufe zu einer Kette zusammen, die sich aus der Abhängigkeit der Objekte ergibt. Die Methode schreibt alle Daten in den Puffer. Implementierung aus der Schnittstelle Flushable.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() löst keine Exception aus. Implementierung aus der Schnittstelle Closeable.static Writer nullWriter()
Ein Writer, der alle Zeichen verwirft. Die Rückgabe ist mit dem Unix-Device /dev/null vergleichbar. Neu in Java 11.Eine übergebene null-Referenz führt immer zur Ausnahme.
19.4.5 Die Schnittstelle Appendable *
Alle Writer und auch die Klassen PrintStream, CharBuffer sowie StringBuffer und StringBuilder implementieren die Schnittstelle Appendable, die drei Methoden vorschreibt:
interface java.io.Appendable
Appendable append(char c)
Hängt das Zeichen c an das aktuelle Appendable an und liefert das aktuelle Objekt vom Typ Appendable wieder zurück.Appendable append(CharSequence csq)
Hängt die Zeichenfolge an dieses Appendable an und liefert es wieder zurück.Appendable append(CharSequence csq, int start, int end)
Hängt einen Teil der Zeichenfolge an dieses Appendable an und liefert es wieder zurück.
Kovariante Rückgabe in Writer von Appendable
Die Klasse Writer demonstriert gut einen kovarianten Rückgabetyp, also dass der Rückgabetyp einer überschriebenen oder implementierten Methode ebenfalls ein Untertyp sein kann. So verfährt auch Writer, der die Schnittstelle Appendable implementiert. Die Methode append(…) in Writer besitzt nicht einfach den Rückgabetyp Appendable aus der Schnittstelle Appendable, sondern konkretisiert ihn zu Writer, das ein Untertyp von Appendable ist.
public Writer append( char c ) throws IOException {
write( c );
return this;
}
19.4.6 Die abstrakte Basisklasse Reader
Die abstrakte Klasse Reader dient dem Lesen von Zeichen aus einem zeichengebenden 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 write(char[], int, int) und close() implementieren müssen – und flush(), was es bei lesenden Strömen nicht gibt. Es bleiben demnach für die Reader-Klasse zwei abstrakte Methoden übrig. Die Unterklassen implementieren jedoch auch andere Methoden aus Geschwindigkeitsgründen neu.
protected Reader()
Erzeugt einen neuen Reader, der sich mit sich selbst synchronisiert.protected Reader(Object lock)
Erzeugt einen neuen Reader, der mit dem Objekt lock synchronisiert ist.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. Die Methode gibt die Anzahl gelesener Zeichen zurück oder –1, wenn das Ende des Stroms erreicht wurde.int read(CharBuffer target) throws IOException
Liest Zeichen in den CharBuffer. Die Methode schreibt die Schnittstelle Readable vor.int read() throws IOException
Die parameterlose Methode liest das nächste Zeichen aus dem Eingabestrom. Sie wartet, wenn kein Zeichen im Strom bereitliegt. Der Rückgabewert ist ein int im Bereich von 0 bis 65.635 (0x0000–0xFFFF). Warum dann der Rückgabewert aber int und nicht char ist, kann leicht damit erklärt werden, dass die Methode den Rückgabewert –1 (0xFFFFFFFF) kodieren muss, falls der Datenstrom keine Daten mehr liefert.int read(char[] cbuf) throws IOException
Liest Zeichen aus dem Strom und schreibt sie in ein Array. Die Methode wartet, bis Eingaben anliegen. Der Rückgabewert ist die Anzahl der gelesenen Zeichen oder –1, wenn das Ende des Datenstroms erreicht wurde.long transferTo(Writer out) throws IOException
Liest alle Zeichen aus diesen Strom und schreibt sie nach out. Neu in Java 10.abstract void close() throws IOException
Schließt den Strom. Folgt anschließend noch ein Aufruf von read(…), mark(int) oder reset(), lösen die Methoden eine IOException aus. Ein doppelt geschlossener Stream hat keinen weiteren Effekt.static Reader nullReader()
Liefert einen Reader, der keine Zeichen liest. Neu in Java 11.
Get Ready
Zu diesen notwendigen Methoden, die bei der Klasse Reader gegeben sind, kommen noch weitere interessante Methoden 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 Standardimplementierung der abstrakten Klasse Reader gibt immer false zurück.
[»] Hinweis
Nehmen wir an, der Datenstrom soll komplett leergesaugt werden, bis keine Daten mehr verfügbar sind und der Datenstrom am Ende ist:
for ( int c; (c = reader.read()) != -1; )
System.out.println( (char) c );
Wir könnten auf die Idee kommen, die Lösung anders zu realisieren:
while ( reader.ready() )
System.out.println( (char) reader.read() );
Allerdings ist die Semantik eine ganz andere: Hier wird lediglich so lange gelesen, bis entweder der Datenstrom leer ist oder – und das ist der Punkt – es zum Blockieren kommt, wenn etwa über das Netzwerk keine Daten verfügbar sind und es etwas dauert, bis Nachschub kommt. Das heißt in diesem Fall: Wir bekommen nur die Daten bis zur Blockade, nicht aber alle Daten.
abstract class java.io.Reader
implements Readable, Closeable
boolean ready() throws IOException
Liefert true, wenn aus dem Stream direkt gelesen werden kann. Das heißt allerdings nicht, dass false immer Blockieren bedeutet.
[»] Hinweis
InputStream und Reader sind sich zwar sehr ähnlich, aber ein InputStream deklariert keine Methode ready(). Dafür gibt es in InputStream eine Methode available(), die sagt, wie viele Bytes ohne Blockierung gelesen werden können. Diese Methode gibt es wiederum nicht im Reader.
Sprünge und Markierungen
Mit der Methode mark(int) lässt sich die Position markieren, an der der Reader gerade steht. Ein Aufruf der Methode reset() setzt den Eingabestrom auf diese Position zurück. Das heißt, diese Stelle lässt sich zu einem späteren Zeitpunkt wieder anspringen. mark(int readAheadLimit) besitzt einen Ganzzahlparameter (int, nicht long), 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 diesen Rücksprung. 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(int) und reset() 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.
abstract class java.io.Reader
implements Readable, Closeable
long skip(long n) throws IOException
Überspringt n Zeichen. Blockt, bis Zeichen vorhanden sind. Gibt die Anzahl der wirklich übersprungenen Zeichen zurück.boolean markSupported()
Der Stream unterstützt die mark()-Operation.void mark(int readAheadLimit) throws IOException
Markiert eine Position im Stream. Der Parameter bestimmt, nach wie vielen Zeichen die Markierung ungültig wird, mit anderen Worten: Er gibt die Puffergröße an.void reset() throws IOException
Falls eine Markierung existiert, setzt der Stream an der Markierung an. Wurde die Position vorher nicht gesetzt, dann wird eine IOException mit dem String »Stream not marked« ausgelöst. Die API-Dokumentation lässt es mit der Bemerkung »könnte fehlschlagen« offen, wie die Methode reagieren soll, wenn in der Zwischenzeit mehr als readAheadLimit Zeichen gelesen wurden.
Reader implementiert die schon bekannte Schnittstelle Closeable mit der Methode close(). Und so, wie ein Writer die Schnittstelle Appendable implementiert, so implementiert ein Reader die Schnittstelle Readable und damit die Operation int read(CharBuffer target) throws IOException.
19.4.7 Die Schnittstellen Closeable, AutoCloseable und Flushable
Zwei besondere Schnittstellen, Closeable und Flushable, schreiben Methoden vor, die alle Ressourcen implementieren, die geschlossen werden und/oder Daten aus einem internen Puffer herausschreiben sollen.
Closeable
Alle lesenden und schreibenden Datenstromklassen, die geschlossen werden können, implementieren Closeable. In der Java SE sind das alle Reader/Writer- und InputStream/OutputStream-Klassen und weitere Klassen wie java.net.Socket.
interface java.io.Closeable
extends AutoClosable
void close() throws IOException
Schließt den Datenstrom. Einen geschlossenen Strom noch einmal zu schließen, hat keine Konsequenz.
Die Schnittstelle Closeable erweitert java.lang.AutoCloseable (siehe Ablauf einer Ausnahmesituation), sodass alles, was Closeable implementiert, damit vom Typ AutoCloseable ist und als Variable bei einem try mit Ressourcen verwendet werden kann.
interface java.lang.AutoClosable
void close() throws Exception
Schließt den Datenstrom. Einen geschlossenen Strom noch einmal zu schließen, hat keine Konsequenz.
[»] Hinweis
Jeder InputStream, OutputStream, Reader und Writer implementiert close() – und mit dem close() auch den Zwang, eine geprüfte IOException zu behandeln. Bei einem Eingabestrom ist die Exception nahezu wertlos und kann auch tatsächlich ignoriert werden. Bei einem Ausgabestrom ist die Exception schon deutlich wertvoller. Das liegt an der Aufgabe von close(), die nicht nur darin besteht, die Ressource zu schließen, sondern vorher noch gepufferte Daten zu schreiben. Somit ist ein close() oft ein indirektes write(…), und hier ist es sehr wohl wichtig zu wissen, ob alle Restdaten korrekt geschrieben wurden. Die Ausnahme sollte auf keinen Fall ignoriert werden, und der catch-Block darf nicht einfach leer bleiben; Logging ist hier das Mindeste.
Flushable
Flushable findet sich nur bei schreibenden Klassen und ist insbesondere bei den Klassen wichtig, die Daten puffern:
interface java.io.Flushable
void flush() throws IOException
Schreibt gepufferte Daten in den Strom.
Die Basisklassen Writer und OutputStream implementieren diese Schnittstelle, aber auch Formatter tut dies.