12.4 Binäre Ein-/Ausgabe-Klassen InputStream und 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 Klasse 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 Bytes 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 )
Schreibt ein einzelnes Byte in den Datenstrom. |
|
void write( byte b[] )
Schreibt die Bytes aus dem Array in den Strom. |
|
void write( byte b[], int off, int len )
Liest len-Byte ab Position off aus dem Array und schreibt ihn in den Ausgabestrom. |
|
void flush()
Gepufferte Daten werden geschrieben. Einzige Methode aus der Schnittstelle Flushable. |
|
void close()
Schließt den Datenstrom. Einzige Methode aus Closeable. |
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, der 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 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 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 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 nativen 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 das.
 12.4.2 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.7
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 ) {}
}
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.
12.4.3 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 )
Erzeugt einen FileOutputStream mit einem gegebenen Dateinamen. |
|
FileOutputStream( File file )
Erzeugt einen FileOutputStream aus einem File-Objekt. |
|
FileOutputStream( FileDescriptor fdObj )
Erzeugt einen FileOutputStream aus einem FileDescriptor-Objekt. |
|
FileOutputStream( String name, boolean append )
Wie FileOutputStream(name), hängt jedoch bei append=true Daten an. |
|
FileOutputStream( File file, boolean append )
Wie FileOutputStream(file), hängt jedoch bei append=true Daten an. |
Ist der Parameter append nicht mit true belegt, wird der alte Inhalt überschrieben.
Das nachfolgende Programm erfragt über einen grafischen Dialog eine Eingabe und schreibt diese in eine Datei:
Listing 12.8
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.4 Die 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()
Gibt die Anzahl der verfügbaren Zeichen im Datenstrom zurück, die sofort ohne Blockierung gelesen werden können. |
|
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. |
|
int read( byte b[] )
Mehrere Bytes werden in ein Feld gelesen. Die tatsächliche Länge der gelesenen Bytes wird zurückgegeben. |
|
int read( byte b[], 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. |
|
long skip( long n )
Überspringt eine Anzahl von Zeichen. |
|
void mark( int readlimit )
Merkt sich eine Position im Datenstrom. |
|
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 reset()
Springt wieder zurück zur Position, die mit mark() gesetzt wurde. |
|
void close()
Schließt den Datenstrom. Operation aus der Schnittstelle Closeable. |
Gelingt eine Durchführung nicht, bekommen wir eine IOException.
12.4.5 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
class java.io. FileInputStream
extends InputStream
|
Um ein Objekt anzulegen, haben wir die Auswahl zwischen drei Konstruktoren:
|
FileInputStream( String name )
Erzeugt einen FileInputStream mit einem gegebenen Dateinamen. Der richtige Dateitrenner, zum Beispiel »\« oder »/«, sollte beachtet werden. |
|
FileInputStream( File file )
Erzeugt FileInputStream aus einem File-Objekt. |
|
FileInputStream( FileDescriptor fdObj )
Erzeugt FileInputStream aus einem FileDescriptor-Objekt. |
Ein Programm, welches seinen eigenen Quellcode anzeigt, sieht wie folgt aus:
Listing 12.9
ReadQuellcode.java
import java.io.*;
public static void main ( String args[] )
{
String filename = "ReadQuellcode.java";
byte buffer[] = new byte[ 4000 ];
FileInputStream in = null;
try
{
in = new FileInputStream( filename );
int len = in.read( buffer, 0, 4000 );
String str = new String( buffer, 0, len );
System.out.println( str );
}
catch ( IOException e ) { System.out.println( e ); }
finally
{
try {
if ( in != null ) in.close();
} catch (IOException e) {}
}
}
}
Zunächst reserviert das Programm ein fixes Bytefeld mit 4 KB. Anschließend wird versucht, 4.000 Zeichen in das Bytefeld einzulesen. Die genaue Anzahl der gelesenen Zeichen liefert die Rückgabe von read(). Anschließend wird das Bytefeld in ein String konvertiert und dieser ausgegeben.
Um die gesamte Datei einzulesen, müssen wir vorher die Dateigröße kennen. Dazu lässt sich length() der File-Klasse nutzen. Und wenn ein File-Objekt sowieso schon angelegt ist, lässt sich damit auch gleich die Datei öffnen.
File f = new File( Dateiname );
byte buffer[] = new byte[ (int) f.length() ];
in = new FileInputStream( f );
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 nicht mit einem 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.
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.
12.4.6 Kopieren von Dateien
 
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 diesem Namen 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 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 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 bringt eine Pufferung über eine Zwischen-Puffer-Klasse keine zusätzliche Geschwindigkeit ein, da wir ja selbst einen 64 KB-Puffer einrichten.
Listing 12.10
FileCopy.java
import java.io.*;
public class FileCopy
{
static void copy( String src, String dest )
{
try
{
copy( new FileInputStream( src ), new FileOutputStream( dest ) );
}
catch( IOException e ) {
System.err.println( e );
}
}
static void copy( InputStream fis, OutputStream fos )
{
try
{
byte buffer[] = new byte[0xffff];
int nbytes;
while ( (nbytes = fis.read(buffer)) != –1 )
fos.write( buffer, 0, nbytes );
}
catch( IOException e ) {
System.err.println( e );
}
finally {
if ( fis != null )
try {
fis.close();
} catch ( IOException e ) {}
try {
if ( fos != null )
fos.close();
} catch ( IOException 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] );
}
}
|