12.4 Binäre Ein-/Ausgabe-Klassen InputStream/OutputStream
 
Die Klassen InputStream und OutputStream bilden die Basisklassen für alle Byte-orientierten 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.
12.4.1 Die abstrakte Basisklasse OutputStream
 
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 konkreter Stream, der in Dateien schreibt, nun weiß, wie er Byte in Dateien schreiben wird. (Natürlich ist hier auch Java mit seiner Plattformunabhängigkeit am Ende, und es werden native Methoden eingesetzt.)
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
Liest len-Byte ab Position off aus dem Array und schreibt ihn in den Ausgabestrom. |
|
void close() throws IOException
Schließt den Datenstrom. Einzige Methode aus Closeable. |
|
void flush() throws IOException
Gepufferte Daten werden geschrieben. Einzige Methode aus der Schnittstelle Flushable. |
Die IOException ist keine RuntimeException, muss also behandelt werden.
Zwei Eigenschaften lassen sich an den Methoden ablesen: zum einen, dass nur Bytes geschrieben werden, und zum anderen, dass nicht wirklich alle Methoden abstract sind. Zur ersten Eigenschaft: Wenn nur Bytes geschrieben werden, bedeutet dies, dass andere Klassen Erstere 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 würde aber bedeuten, dass alle anderen konkret wären. Gleichzeitig stellt sich die Frage, wie ein OutputStream, der die Eigenschaften für alle erdenklichen Ausgabeströme vorschreibt, denn wissen kann, wie ein spezieller Ausgabestrom etwa geschlossen (close()) wird oder seine gepufferten Bytes schreibt (flush()). Das weiß 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 sie sie gar nicht nutzen wollen.
Über konkrete und abstrakte Schreibmethoden
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 werfen einen Blick in den Quellcode der Bibliothek:
public void write(byte<span class="listing">[]</span> b) throws IOException {
write(b, 0, b.length);
}
public void write(byte<span class="listing">[]</span> 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 ganze Bytefelder schreibt, etwa eine 5 MB-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, denn wir haben in dem Modell vergessen, dass die Unterklasse zwar die abstrakte Methode implementieren muss, aber immer noch andere Methoden überschreiben kann. Ein späterer Blick auf die Klasse FileOutputStream bestätigt dies.
Hinweis Ruft eine Oberklasse eine abstrakte Methode auf, die in der Unterklasse implementiert wird, ist das ein Entwurfsmuster mit dem Namen Schablonen-Muster engl. Template-Pattern.
|
12.4.2 Die Schnittstellen Closeable und Flushable
 
Closeable und Flushable sind zwei sehr einfache Schnittstellen seit Java 5. Closeable wird von allen lesenden und schreibenden Datenstrom-Klassen implementiert, die geschlossen werden können.
|
void close() throws IOException
Schließt den Datenstrom. Ein schon geschlossener Strom kann noch einmal geschlossen werden und hat keine Konsequenzen. |
Flushable findet sich nur bei allen schreibenden Klassen, und ist insbesondere bei denen wichtig, die Daten Puffern.
|
void flush() throws IOException
Schreibt gepufferte Daten in den Strom. |
12.4.3 Ein Datenschlucker
 
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.8
NullOutputStream.java
import java.io.OutputStream;
public final class NullOutputStream extends OutputStream
{
@Override public void write( byte[] b ) { /* Empty */ }
@Override public void write( byte[] b, int off, int len ) { /* Empty */ }
@Override public void write( int b ) { /* Empty */ }
}
Da close() und flush()ohnehin schon mit einem leeren Block implementiert sind, brauchen wir sie nicht noch einmal zu überschreiben. Aus Effizienzgründen (!) geben wir auch eine Implementierung für die Schreib-Feld-Methoden an.
12.4.4 Anwendung der Klasse FileOutputStream
 
Diese Klasse FileOutputStream bietet grundlegende Schreibmethoden, um in Dateien zu schreiben. FileOutputStream implementiert alle nötigen Methoden, die OutputStream vorschreibt.
class java.io. FileOutputStream
extends OutputStream
|
|
FileOutputStream( String name ) throws FileNotFoundException
Erzeugt einen FileOutputStream mit einem gegebenen Dateinamen. |
|
FileOutputStream( File file ) throws FileNotFoundException
Erzeugt einen FileOutputStream aus einem File-Objekt. |
|
FileOutputStream( String name, boolean append ) throws FileNotFoundException
Wie FileOutputStream(name), hängt jedoch bei append=true Daten an. |
|
FileOutputStream( File file, boolean append ) throws FileNotFoundException
Wie FileOutputStream(file), hängt jedoch bei append=true Daten an. |
|
FileOutputStream( FileDescriptor fdObj )
Erzeugt einen FileOutputStream aus einem FileDescriptor-Objekt. |
Ist der Parameter append nicht mit true belegt, wird der alte Inhalt überschrieben. Die FileNotFoundException wirkt vielleicht etwas komisch, wird aber dann ausgelöst, wenn zum Beispiel die Dateiangabe ein Verzeichnis repräsentiert oder die Datei gelockt ist.
Das nachfolgende Programm erfragt über einen grafischen Dialog eine Eingabe und schreibt diese in eine Datei:
Listing 12.9
BenutzereingabeSchreiben.java
import java.io.*;
import javax.swing.JOptionPane;
public class BenutzereingabeSchreiben
{
public static void main( String[] args )
{
byte[] buffer = new byte[80];
try
{
String s;
while ( ( s = JOptionPane.showInputDialog( "Gib eine nette Zeile ein:" )) == null )
{ }
FileOutputStream fos = new FileOutputStream( "c:/line.txt" );
fos.write( s.getBytes() );
fos.close();
}
catch ( Exception e ) { System.out.println( e ); }
}
}
12.4.5 Die abstrakte Eingabeklasse 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.
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. |
|
int read() throws IOException
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. |
|
int read( byte[] b ) throws IOException
Mehrere Byte werden in ein Feld gelesen. Die tatsächliche Länge der gelesenen Bytes wird zurückgegeben. |
|
int read( byte[] b, int off, int len ) throws IOException
Liest den Datenstrom in ein Bytefeld, schreibt ihn aber erst an der Stelle off in das Bytefeld. Zudem begrenzt len die maximale Anzahl der zu lesenden Zeichen. |
|
long skip( long n ) throws IOException
Überspringt eine Anzahl von Zeichen. |
|
void close() throws IOException
Schließt den Datenstrom. Operation aus der Schnittstelle Closeable. |
|
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. |
|
void mark( int readlimit )
Merkt sich eine Position im Datenstrom. |
|
void reset() throws IOException
Springt wieder zurück zur Position, die mit mark() gesetzt wurde. |
Auffällig ist, dass bis auf mark() und markSupported() alle Methoden im Fehlerfall eine IOException auslösen.
Hinweis available() liefert die Anzahl Byte, 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.
|
12.4.6 Ressourcen wie Grafiken aus dem Klassenpfad und aus Jar–Archiven laden
 
Um Ressourcen wie Grafiken oder Konfigurationsdateien aus Jar-Archiven zu laden, ist die Methode getResourceAsStream() beziehungsweise getResource() ideal. Sie sind Methoden des Class-Objekts. getResource() gibt ein URL-Objekt für die Ressource zurück. Da oft der Inhalt des Datenstroms interessant ist, liefert getResourceAsStream() einen InputStream. Intern wird aber nichts anderes gemacht, als getResource() aufgerufen und mit openStream() ein Eingabe-Objekt geholt. Nur getResourceAsStream() fängt eine eventuelle IOException ab und liefert dann die Rückgabe null.
Da der Klassenlader die Ressource findet, entdeckt er alle Dateien, die im Pfad des Klassenladers eingetragen sind. Das gilt auch für Jar-Archive, weil dort vom Klassenlader alles verfügbar ist. Falls die Quelle nicht aufgelöst werden konnte, liefern die Methoden null. null wird auch zurückgegeben, wenn die Sicherheitsrichtlinien das Lesen verbieten.
Beispiel Besorge einen Eingabestrom in1 auf die Datei kullin_fun.txt und einen zweiten Eingabestrom in2 auf die Datei hirse_fun.jpg innerhalb der Objektmethode lade().
class C
{
InputStream in1 = C.class.getResourceAsStream( "kullin_fun.txt" );
void lade()
{
InputStream in2 = getClass().getResourceAsStream( "hirse_fun.jpg" );
}
}
|
Da zum Nutzen der getResourceXXX()-Methoden ein Class-Objekt nötig ist, zeigt das Beispiel zum einen, dass über C.class das Class-Objekt zu bekommen ist, und zum anderen, dass in einer Objektmethode ebenfalls die geerbte Object-Methode getClass() ein Class-Objekt liefert.
12.4.7 Anwenden der Klasse FileInputStream
 
Bisher haben wir die grundlegenden Ideen der Stream-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 (etwa repräsentiert als ein Objekt vom Typ File) an einen Datenstrom.
 Hier klicken, um das Bild zu Vergrößern
Um ein Objekt anzulegen, haben wir die Auswahl zwischen drei Konstruktoren.
class java.io. FileInputStream
extends InputStream
|
|
FileInputStream( String name ) throws FileNotFoundException
Erzeugt einen FileInputStream mit einem gegebenen Dateinamen. Der richtige Dateitrenner, zum Beispiel »\« oder »/«, sollte beachtet werden. |
|
FileInputStream( File file ) throws FileNotFoundException
Erzeugt FileInputStream aus einem File-Objekt. |
|
FileInputStream( FileDescriptor fdObj )
Erzeugt FileInputStream aus einem FileDescriptor-Objekt. |
Hinweis Lese den ganzen Dateiinhalt in ein Bytefeld.
File f = new File( dateiname );
byte[] buffer = new byte[ (int) f.length() ];
InputStream in = new FileInputStream( f );
Sinnvoller als das gesamte Einlesen ist aber im Allgemeinen das Lesen in Blöcken.
|
12.4.8 Kopieren von Dateien
 
Als Beispiel für das Zusammenspiel von FileInputStream und FileOutputStream wollen wir 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 erledigen wir einfach mit FileOutputStream. Der Methode ist es jedoch relativ gleichgültig, ob es bereits eine Datei dieses Namens gibt, da sie diese gnadenlos überschreibt. Auch darum kümmern wir uns nicht. Wollten wir das berücksichtigen, sollten wir mit Hilfe der File-Klasse die Existenz einer gleichnamigen Datei prüfen. 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 dieser Ansatz in Bezug auf die Geschwindigkeit erbärmlich ist. Das Puffern in einen BufferedInputStream beziehungsweise Ausgabestrom ist in diesem Fall 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 erbringt eine Pufferung über eine Zwischen-Puffer-Klasse keinen Geschwindigkeitsgewinn, da wir ja selbst einen 64 KB-Puffer einrichten.
Listing 12.10
FileCopy.java
import java.io.*;
public class FileCopy
{
static void copy( InputStream fis, OutputStream fos )
{
try
{
byte[] buffer = new byte[ 0xFFFF ];
for ( int len; (len = fis.read(buffer)) != –1; )
fos.write( buffer, 0, len );
}
catch( IOException e ) {
System.err.println( e );
}
finally {
if ( fis != null )
try { fis.close(); } catch ( IOException e ) { e.printStackTrace(); }
if ( fos != null )
try { fos.close(); } catch ( IOException e ) { e.printStackTrace(); }
}
}
static void copyFile( String src, String dest )
{
try
{
copy( new FileInputStream( src ), new FileOutputStream( dest ) );
}
catch( IOException e ) {
e.printStackTrace();
}
}
public static void main( String[] args )
{
if ( args.length != 2 )
System.err.println( "Usage: java FileCopy <src> <dest>" );
else
copyFile( args[0], args[1] );
}
}
12.4.9 Das FileDescriptor-Objekt
 
Die Klasse java.io.FileDescriptor repräsentiert eine offene Datei oder eine Socket-Verbindung mittels eines Deskriptors. Er lässt sich bei File-Objekten mit getFD() erfragen; bei Socket-Verbindungen allerdings nicht über eine Funktion – nur Unterklassen von SocketImpl (und DatagramSocketImpl) ist der Zugriff auf eine protected Methode getFileDescriptor() zugesagt.
In der Regel kommt der Entwickler mit keinem FileDescriptor-Objekt in Kontakt. Es gibt allerdings eine Anwendung, in der die Klasse FileDescriptor nützlich ist: Sie bietet eine sync()-Funktion an, die verbleibende Speicherblöcke auf das Gerät schreibt. Damit lässt sich erreichen, dass Daten auch tatsächlich auf dem Datenträger materialisiert werden.
FileOutputStream os = new FileOutputStream(...);
FileDescriptor fd = os.getFD();
fd.sync();
Neben FileInputStream kennen auch FileOutputStream und RandomAccessFile eine Funktion getFD(). Mit einem FileDescriptor kann auch die Arbeit zwischen Stream-Objekten und RandomAccessFile-Objekten koordiniert werden.
final class java.io. FileDeskriptor
|
|
void sync()
Materialisiert die Daten. sync() kehrt erst dann zurück, wenn die Daten geschrieben wurden. |
|