Galileo Computing < openbook > Galileo Computing - Professionelle Bücher. Auch für Einsteiger.
Professionelle Bücher. Auch für Einsteiger

Java ist auch eine Insel von Christian Ullenboom
Programmieren für die Java 2-Plattform in der Version 5 (Tiger-Release)
Buch: Java ist auch eine Insel
gp Kapitel 12 Datenströme und Dateien
  gp 12.1 Datei und Verzeichnis
    gp 12.1.1 Dateien und Verzeichnisse mit der Klasse File
    gp 12.1.2 Dateieigenschaften und -attribute
    gp 12.1.3 Dateien berühren, neue Dateien anlegen
    gp 12.1.4 Umbenennen und Verzeichnisse anlegen
    gp 12.1.5 Die Wurzel aller Verzeichnisse/Laufwerke
    gp 12.1.6 Verzeichnisse listen und Dateien filtern
    gp 12.1.7 Dateien und Verzeichnisse löschen
    gp 12.1.8 Implementierungsmöglichkeiten für die Klasse File
    gp 12.1.9 Verzeichnisse nach Dateien rekursiv durchsuchen
    gp 12.1.10 Sicherheitsprüfung
    gp 12.1.11 Namen der Laufwerke
    gp 12.1.12 Locking
  gp 12.2 Dateien mit wahlfreiem Zugriff
    gp 12.2.1 Ein RandomAccessFile öffnen
    gp 12.2.2 Aus dem RandomAccessFile lesen
    gp 12.2.3 Schreiben
    gp 12.2.4 Die Länge des RandomAccessFile
    gp 12.2.5 Hin und her in der Datei
  gp 12.3 Stream-Klassen und Reader/Writer
    gp 12.3.1 Die abstrakten Basisklassen
    gp 12.3.2 Übersicht über Ein-/Ausgabeklassen
  gp 12.4 Binäre Ein-/Ausgabe-Klassen InputStream und OutputStream
    gp 12.4.1 Die Klasse OutputStream
    gp 12.4.2 Ein Datenschlucker
    gp 12.4.3 Anwendung der Klasse FileOutputStream
    gp 12.4.4 Die Eingabeklasse InputStream
    gp 12.4.5 Anwenden der Klasse FileInputStream
    gp 12.4.6 Kopieren von Dateien
  gp 12.5 Daten filtern durch FilterInputStream und FilterOutputStream
    gp 12.5.1 Der besondere Filter PrintStream
    gp 12.5.2 Die Schnittstelle Appendable
    gp 12.5.3 System.in und System.out
  gp 12.6 Besondere OutputStream- und InputStream-Klassen
    gp 12.6.1 Bytes in den Strom schreiben mit ByteArrayOutputStream
    gp 12.6.2 Bytes in den Strom schreiben mit ByteArrayInputStream
    gp 12.6.3 Ströme zusammensetzen mit SequenceInputStream
  gp 12.7 Ressourcen wie Grafiken aus dem Klassenpfad und aus Jar-Archiven laden
  gp 12.8 Die Unterklassen von Writer
    gp 12.8.1 Die abstrakte Klasse Writer
    gp 12.8.2 Datenkonvertierung durch den OutputStreamWriter
    gp 12.8.3 In Dateien schreiben mit der Klasse FileWriter
    gp 12.8.4 StringWriter und CharArrayWriter
    gp 12.8.5 Writer als Filter verketten
    gp 12.8.6 Gepufferte Ausgabe durch BufferedWriter
    gp 12.8.7 Ausgabemöglichkeiten durch PrintWriter erweitern
    gp 12.8.8 Daten mit FilterWriter filtern
  gp 12.9 Die Klassen um Reader
    gp 12.9.1 Die abstrakte Basisklasse Reader
    gp 12.9.2 Automatische Konvertierungen mit dem InputStreamReader
    gp 12.9.3 Dateien lesen mit der Klasse FileReader
    gp 12.9.4 StringReader und CharArrayReader
  gp 12.10 Die Filter für Zeichenströme
    gp 12.10.1 Gepufferte Eingaben mit der Klasse BufferedReader
    gp 12.10.2 LineNumberReader zählt automatisch Zeilen mit
    gp 12.10.3 Eingaben filtern mit der Klasse FilterReader
    gp 12.10.4 Daten mit der Klasse PushbackReader zurücklegen
  gp 12.11 Kommunikation zwischen Threads mit Pipes
    gp 12.11.1 PipedOutputStream und PipedInputStream
    gp 12.11.2 PipedWriter und PipedReader
  gp 12.12 Datenkompression
    gp 12.12.1 Die Java-Unterstützung beim Komprimieren und Zusammenpacken
    gp 12.12.2 Datenströme komprimieren
    gp 12.12.3 Zip-Archive
    gp 12.12.4 Jar-Archive
  gp 12.13 Prüfsummen
    gp 12.13.1 Die Schnittstelle Checksum
    gp 12.13.2 Die Klasse CRC32
    gp 12.13.3 Die Adler32-Klasse
  gp 12.14 Persistente Objekte und Serialisierung
    gp 12.14.1 Objekte speichern mit der Standard-Serialisierung
    gp 12.14.2 Objekte über die Standard-Serialisierung lesen
    gp 12.14.3 Die Schnittstelle Serializable
    gp 12.14.4 Nicht serialisierbare Attribute mit transient aussparen
    gp 12.14.5 Das Abspeichern selbst in die Hand nehmen
    gp 12.14.6 Tiefe Objektkopien
    gp 12.14.7 Versionenverwaltung und die SUID
    gp 12.14.8 Wie die ArrayList serialisiert
    gp 12.14.9 Serialisieren in XML-Dateien
    gp 12.14.10 XML-API von Sun
  gp 12.15 Zugriff auf SMB-Server
    gp 12.15.1 jCIFS
  gp 12.16 Tokenizer
    gp 12.16.1 StreamTokenizer
    gp 12.16.2 CSV (Comma Separated Values)-Dateien verarbeiten
  gp 12.17 Die Logging-API


Galileo Computing

12.4 Binäre Ein-/Ausgabe-Klassen InputStream und OutputStreadowntop

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.


Galileo Computing

12.4.1 Die Klasse OutputStream  downtop

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

gp  abstract void write( int b )
Schreibt ein einzelnes Byte in den Datenstrom.
gp  void write( byte b[] )
Schreibt die Bytes aus dem Array in den Strom.
gp  void write( byte b[], int off, int len )
Liest len-Byte ab Position off aus dem Array und schreibt ihn in den Ausgabestrom.
gp  void flush()
Gepufferte Daten werden geschrieben. Einzige Methode aus der Schnittstelle Flushable.
gp  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.


Galileo Computing

12.4.2 Ein Datenschlucker  downtop

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.


Galileo Computing

12.4.3 Anwendung der Klasse FileOutputStream  downtop

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

gp  FileOutputStream( String name )
Erzeugt einen FileOutputStream mit einem gegebenen Dateinamen.
gp  FileOutputStream( File file )
Erzeugt einen FileOutputStream aus einem File-Objekt.
gp  FileOutputStream( FileDescriptor fdObj )
Erzeugt einen FileOutputStream aus einem FileDescriptor-Objekt.
gp  FileOutputStream( String name, boolean append )
Wie FileOutputStream(name), hängt jedoch bei append=true Daten an.
gp  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); }
  }
}

Galileo Computing

12.4.4 Die Eingabeklasse InputStream  downtop

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

gp  int available()
Gibt die Anzahl der verfügbaren Zeichen im Datenstrom zurück, die sofort ohne Blockierung gelesen werden können.
gp  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.
gp  int read( byte b[] )
Mehrere Bytes werden in ein Feld gelesen. Die tatsächliche Länge der gelesenen Bytes wird zurückgegeben.
gp  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.
gp  long skip( long n )
Überspringt eine Anzahl von Zeichen.
gp  void mark( int readlimit )
Merkt sich eine Position im Datenstrom.
gp  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.
gp  void reset()
Springt wieder zurück zur Position, die mit mark() gesetzt wurde.
gp  void close()
Schließt den Datenstrom. Operation aus der Schnittstelle Closeable.

Gelingt eine Durchführung nicht, bekommen wir eine IOException.


Galileo Computing

12.4.5 Anwenden der Klasse FileInputStream  downtop

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.

Abbildung
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:

gp  FileInputStream( String name )
Erzeugt einen FileInputStream mit einem gegebenen Dateinamen. Der richtige Dateitrenner, zum Beispiel »\« oder »/«, sollte beachtet werden.
gp  FileInputStream( File file )
Erzeugt FileInputStream aus einem File-Objekt.
gp  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.


Galileo Computing

12.4.6 Kopieren von Dateien  toptop

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] );
  }
}




Copyright © Galileo Press GmbH 2004
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das <openbook> denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.


[Galileo Computing]

Galileo Press GmbH, Gartenstraße 24, 53229 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, info@galileo-press.de