Galileo Computing < openbook >
Galileo Computing - Programming the Net
Galileo Computing - Programming the Net


Java ist auch eine Insel (2. Aufl.) von Christian Ullenboom
Programmieren für die Java 2-Plattform in der Version 1.4
Java ist auch eine Insel (2. Auflage)
gp Kapitel 12 Datenströme und Dateien
  gp 12.1 Dateien und Verzeichnisse
  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 Hin und her in der Datei
    gp 12.2.4 Die Länge des RandomAccessFile
  gp 12.3 Übersicht über wichtige Stream- und WriterReader
    gp 12.3.1 Die abstrakten Basisklassen
  gp 12.4 Eingabe- und Ausgabe-Klassen: InputStream und OutputStream
    gp 12.4.1 Die Klasse OutputStream
    gp 12.4.2 Ein Datenschlucker
    gp 12.4.3 Die Eingabeklasse InputStream
    gp 12.4.4 Anwenden der Klasse FileInputStream
    gp 12.4.5 Anwendung der Klasse FileOutputStream
    gp 12.4.6 Kopieren von Dateien
    gp 12.4.7 Daten filtern durch FilterInputStream und FilterOutputStream
    gp 12.4.8 Der besondere Filter PrintStream
    gp 12.4.9 System.in und System.out
    gp 12.4.10 Bytes in den Strom mit ByteArrayOutputStream
    gp 12.4.11 Ströme zusammensetzen mit SequenceInputStream
  gp 12.5 Ressourcen wie Grafiken aus dem Klassenpfad und aus Jar-Archiven laden
  gp 12.6 Die Unterklassen von Writer
    gp 12.6.1 Die abstrakte Klasse Writer
    gp 12.6.2 Datenkonvertierung durch den OutputStreamWriter
    gp 12.6.3 In Dateien schreiben mit der Klasse FileWriter
    gp 12.6.4 StringWriter und CharArrayWriter
    gp 12.6.5 Writer als Filter verketten
    gp 12.6.6 Gepufferte Ausgabe durch BufferedWriter
    gp 12.6.7 Ausgabemöglichkeiten durch PrintWriter erweitern
    gp 12.6.8 Daten mit FilterWriter filtern
  gp 12.7 Die Klassen um Reader
    gp 12.7.1 Die abstrakte Basisklasse Reader
    gp 12.7.2 Automatische Konvertierungen mit dem InputStreamReader
    gp 12.7.3 Dateien lesen mit der Klasse FileReader
    gp 12.7.4 StringReader und CharArrayReader
  gp 12.8 Schachteln von Eingabe-Streams
    gp 12.8.1 Gepufferte Eingaben mit der Klasse BufferedReader
    gp 12.8.2 LineNumberReader zählt automatisch Zeilen mit
    gp 12.8.3 Eingaben filtern mit der Klasse FilterReader
    gp 12.8.4 Daten zurücklegen mit der Klasse PushbackReader
  gp 12.9 Kommunikation zwischen Threads mit Pipes
    gp 12.9.1 PipedOutputStream und PipedInputStream
    gp 12.9.2 PipedWriter und PipedReader
  gp 12.10 Datenkompression
    gp 12.10.1 Datenströme komprimieren
    gp 12.10.2 Zip-Archive
  gp 12.11 Prüfsummen
    gp 12.11.1 Die Schnittstelle Checksum
    gp 12.11.2 Die Klasse CRC32
    gp 12.11.3 Die Adler32-Klasse
  gp 12.12 Persistente Objekte und Serialisierung
    gp 12.12.1 Objekte speichern
    gp 12.12.2 Objekte lesen
    gp 12.12.3 Die Schnittstelle Serializable
    gp 12.12.4 Tiefe Objektkopien
    gp 12.12.5 Felder sind implizit Serializable
    gp 12.12.6 Versionenverwaltung und die SUID
    gp 12.12.7 Beispiele aus den Standard-Klassen
    gp 12.12.8 Serialisieren in XML-Dateien
    gp 12.12.9 JSX (Java Serialization to XML)
    gp 12.12.10 XML-API von Sun
  gp 12.13 Die Logging-API


Galileo Computing

12.8 Schachteln von Eingabe-Streams  downtop

Ebenso wie sich Datenströme in der Ausgabe schachteln lassen, können auch Eingabeströme hintereinander Daten verändern. Folgende Klassen stehen zur Verfügung, die im Konstruktor ein Reader erwarten: BufferedReader, LineNumberReader, FilterReader und PushbackReader. Der Reader wird intern unter der proteced-Variablen in verwaltet.


Galileo Computing

12.8.1 Gepufferte Eingaben mit der Klasse BufferedReader  downtop

Ein BufferedReader puffert ähnlich wie ein BufferedWriter einige Daten vor. Die Daten werden also zuerst in einen kleinen Zwischenspeicher geladen, der wiederum wie beim BufferedWriter 8 KB groß ist. Durch die Bereitstellung der Daten müssen weniger Zugriffe auf den Datenträger vorgenommen werden und die Geschwindigkeit der Anwendung erhöht sich. Aus BufferedReader geht direkt die Unterklasse LineNumberReader hervor, die Zeilennummern zugänglich macht. Da ein BufferedReader Markierungen und Sprünge erlaubt, werden die entsprechenden Funktionen von Reader überschrieben.

Die Klasse BufferedReader besitzt zwei Konstruktoren. Bei einem lässt sich die Größe des internen Puffers angeben.

class java.io.BufferedReader
extends Reader

gp  BufferedReader( Reader in )
Erzeugt einen puffernden Zeichenstrom mit der Puffergröße von 8 k.
gp  BufferedReader( Reader in, int sz )
Erzeugt einen puffernden Zeichenstrom mit der Puffergröße sz.

Zusätzlich stellt BufferedReader die Methode readLine() zur Verfügung, die eine komplette Textzeile liest und als String an den Aufrufer zurückgibt.

gp  String readLine()
Liest eine Zeile bis zum Zeilenende und gibt den String ohne die Endzeichen zurück. null, wenn der Stream am Ende ist.

Textzeilen lesen mit readLine() – früher und heute

Seit der Java-Version 1.1 ist die Methode readLine() aus der Klasse DataInputStream veraltet. Früher war Folgendes üblich, um eine Textzeile von der Konsole zu lesen:

DataInputStream in = new DataInputStream( System.in );
String s = in.readLine();

Heutzutage bietet die Klasse BufferedReader die Methode readLine() an. Die Programme, die den DataInputStream noch für die Zeileneingabe nutzen, sollten mit dem BufferedReader umgeschrieben werden. Somit ergibt sich eine Zeileneingabe von der Konsole nun mit nachfolgenden Zeilen:

BufferedReader in;
in = new BufferedReader( new InputStreamReader(System.in) );
String s = in.readLine();

Eine Textzeile ist durch die Zeichen »\n« oder »\r« begrenzt. Zusätzlich wird auch die Folge der beiden Zeichen beachtet, also »\r\n«. Die Methode basiert auf einer privaten Funktion der Klasse. Sie ruft readLine(boolean skipLF) auf, eine Methode, die auch für uns hin und wieder nützlich wäre. Sie bestimmt, ob die Zeilenendezeichen überlesen werden sollen oder nicht. Im Bedarfsfall bleibt uns nichts anderes übrig, als den Programmcode aus den Originalquellen zu kopieren.

Beispiel   Das folgende Programm implementiert ein einfaches »cat«-Kommando von Unix. Es können auf der Parameterzeile Dateinamen übergeben werden. Die Dateien werden dann in der Standardausgabe ausgegeben.

Listing 12.21   cat.java

import java.io.*;
class cat
{
  public static void main( String args[] )
  {
    try
    {
      for ( int i = 0; i < args.length; i++ )
      {
        BufferedReader in = new BufferedReader( new FileReader(args[i]) );
        String line;
        
        while ( (line = in.readLine()) != null )
          System.out.println( line );
      }
    }
    catch ( IOException e ) {
      System.out.println( e );
    }
  }
}

Galileo Computing

12.8.2 LineNumberReader zählt automatisch Zeilen mit  downtop

LineNumberReader ist die einzige Klasse, die als Unterklasse von BufferedReader aus den Java-Bibliotheken hervorgeht. Ein LineNumberReader liest die Eingabezeilen und zählt gleichzeitig die Zeilen, die gelesen wurden. Mit zwei Funktionen lässt sich auf die Zeilennummern zugreifen: getLineNumber() und setLineNumber(). Dass die Zeilennummer auch geschrieben werden kann, ist sicherlich ungewöhnlich, aber intern wird nur die Variable lineNumber geschrieben. Bei getLineNumber() wird diese Variable zurückgeliefert. Bei jedem read() untersuchen die Funktionen, ob im Eingabestrom ein »\n« oder »\r« vorkommt. Wenn dies der Fall ist, dann inkrementieren sie die Variable lineNumber.

Abbildung

class java.io.LineNumberReader
extends BufferedReader

gp  int getLineNumber()
Liefert die aktuelle Zeilennummer.
gp  void setLineNumber( int lineNumber )
Setzt die aktuelle Zeilennummer.
Beispiel   Die nachfolgende Klasse verbindet einen LineNumberReader mit einer Datei aus einem FileReader. Dann lesen wir die Zeilen mittels readLine() aus. Nun ist es praktisch, dass LineNumberReader eine Erweiterung von BufferedReader ist, die uns diese praktische Funktion vererbt. Wir geben zunächst die Zeilennummer und dann die Zeile selbst aus.

Listing 12.22   LineNumberReaderDemo.java

import java.io.*;

public class LineNumberReaderDemo
{
  public static void main( String args[] )
  {
    try
    {
      String line;
      FileReader fr = new FileReader("LineNumberReaderDemo.java");

      LineNumberReader f = new LineNumberReader( fr );

while ( (line = f.readLine()) != null ) System.out.println( f.getLineNumber() + ": " + line ); f.close(); } catch ( IOException e ) { System.out.println( "Fehler beim Lesen der Datei" ); } } }

Galileo Computing

12.8.3 Eingaben filtern mit der Klasse FilterReader  downtop

Wie das Schachteln von Ausgabeströmen, so ist auch das Verbinden mehrerer Eingabeströme möglich. Als abstrakte Basis-Zwischenklasse existiert hier FilterReader, die ein Reader-Objekt im Konstruktor übergeben bekommt. Dieser sichert den Parameter in der protected-Variablen in. Der Konstruktor ist protected, da auch er von der Unterklasse mit super() aufgerufen werden soll. Dazu lässt sich das Beispiel aus dem FilterWriter noch einmal heranziehen. Alle Aufrufe, die an den FilterReader gehen, werden an den Reader in weitergeleitet, das heißt etwa, wenn der FilterReader geschlossen wird, dann wird der Aufruf in.close() ausgeführt. Aus diesem Grunde muss der FilterReader auch alle Methoden von Reader überschreiben, da ja eine Umleitung stattfindet.

Abbildung

abstract class java.io.FilterReader
extends Reader

gp  protected Reader in
Der Zeicheneingabestrom oder null, wenn der Strom geschlossen wurde.
gp  protected FilterReader( Reader in )
Erzeugt einen neuen filternden Reader.

Die Methoden read(), read(char[] cbuf, int off, int len), skip(long n), ready(), markSupported(), mark(int readAheadLimit), reset() und close() werden überschrieben und leiten die Aufrufe direkt an Reader weiter. Wenn dieser eine Exception wirft, wird sie an uns weitergeleitet.

HTML-Tags überlesen mit einem speziellen Filter

Unser nächstes Beispiel ist eine Klasse, die den FilterReader so erweitert, dass HTML-Tags überlesen werden. Sie werden allerdings nicht so komfortabel wie beim HTMLWriter im Datenstrom umgesetzt. Die Klasse überschreibt den notwendigen Konstruktor und implementiert die beiden read()-Methoden. Die read()-Methode ohne Parameter legt einfach ein ein Zeichen großes Feld an und ruft dann die read()-Methode auf, die die Daten in ein Feld liest. Da dieser Methode neben dem Feld auch noch die Größe übergeben werden kann, müssen wirklich so viele Zeichen gelesen werden. Es reicht nicht einfach aus, die übergebene Anzahl von Zeichen vom Reader in zu lesen, sondern hier müssen wir beachten, dass eingestreute Tags nicht zählen. Die Zeichenkette <p>Hallo<p> ist demnach fünf Zeichen lang und nicht elf. Liegt eine solche Zeichenkette vor, so müssen mehr als vier Zeichen vom darunter liegenden Reader abgenommen werden.

Listing 12.23   HTMLReader.java

import java.io.*;

class HTMLReader extends FilterReader
{
  public HTMLReader( Reader in )
  {
    super( in );
  }

  public int read() throws IOException
  {
    char buf[] = new char[1];
    return read(buf, 0, 1) == -1 ? -1 : buf[0];
  }

  public int
  read( char[] cbuf, int off, int len ) throws IOException
  {
    int numchars = 0;

    while ( numchars == 0 )
    {
      numchars = in.read( cbuf, off, len );

      if ( numchars == -1 )  // EOF?
        return -1;

      int last = off;

      for( int i = off; i < off + numchars; i++ )
      {
        if ( !intag ) {
          if ( cbuf[i] == '<' )
            intag = true;
          else
            cbuf[last++] = cbuf[i];
        }
        else if (cbuf[i] == '>')
          intag = false;
      }
      numchars = last – off;
    }
    return numchars;
  }

  private boolean intag = false;
}


public class HTMLReaderDemo
{
  public static void main( String args[] )
  {
    try {
      String s = "<html>Hallo! <b>Ganz schön fett.</b>"+
                 "Ah, wieder normal.</html>";

      StringReader sr = new StringReader( s );
      HTMLReader hr = new HTMLReader( sr );
      BufferedReader in = new BufferedReader( hr );

      String t;
      while ( (t = in.readLine()) != null )
        System.out.println( t );

      in.close();
    }
    catch( Exception e ) { System.err.println( e ); }
  }
}

Das Programm produziert dann die einfache Ausgabe:

Hallo! Ganz schön fett. Ah, wieder normal.

Der einzige Grund, warum wir auf den HTMLReader noch einen BufferedReader aufsetzen, ist der, dass wir dann die readLine()-Methode nutzen können.


Galileo Computing

12.8.4 Daten zurücklegen mit der Klasse PushbackReader  toptop

Abbildung

Der Eingabefilter PushbackReader ist die einzige Klasse, die direkt aus FilterReader abgeleitet ist. Sie definiert eine Filter-Klasse, die einen Puffer einer beliebigen Größe besitzt, in den Zeichen wieder zurückgeschrieben werden können. Schreiben wir einen Parser, der eine Wahl aufgrund des nächsten gelesenen Zeichens (ein so genannter Vorrausschauender Parser) trifft, dann kann er dieses Zeichen wieder in den Eingabestrom legen, wenn er den Weg doch nicht verfolgen möchte. Hier ist der Einsatz der Klasse PushbackReader angebracht. Denn der nächste Lesezugriff liest dann dieses zurückgeschriebene Zeichen.

class java.io.PushbackReader
extends FilterReader

gp  PushbackReader( Reader in )
Erzeugt einen PushbackReader aus dem Reader in mit der Puffergröße 1.
gp  PushbackReader( Reader in, int size )
Erzeugt einen PushbackReader aus dem Reader in mit der Puffergröße size.

Um ein Zeichen oder eine Zeichenfolge wieder in den Eingabestrom zu legen, wird die Methode unread() ausgeführt.

gp  public void unread( int c ) throws IOException
public void unread( char cbuf[], int off, int len )
throws IOException
public void unread( char cbuf[] ) throws IOException
Legt ein Zeichen oder ein Feld von Zeichen zurück in den Zeichenstrom.

Zeilennummern entfernen

Das nächste Programm demonstriert die Möglichkeiten eines PushbackReaders. Die Implementierung wirkt möglicherweise etwas gezwungen, sie zeigt jedoch, wie unread() eingesetzt werden kann. Das Programm löst folgendes Problem: Wir haben eine Textdatei (im Programm einfach als String über einen StringReader zur Verfügung gestellt), in der Zeilennummern mit dem String verbunden sind.

134Erste Zeile
234Zeile

Wir wollen nun die Zahlen vom Rest der Zeilen trennen. Dazu lesen wir so lange die Zahlen ein, bis ein Zeichen folgt, bei dem Character.isDigit() die Rückgabe false ergibt. Dann wissen wir, dass wir keine Ziffer mehr im Strom haben. Das Problem ist nun, dass ja schon ein Zeichen mehr gelesen wurde. In einem normalen Programm ohne die Option, das Zeichen zurücklegen zu können, würde das etwas ungemütlich. Dieses Zeichen müsste dann gesondert behandelt werden, da es ja das erste Zeichen der neuen Eingabe ist und nicht mehr zur Zahl gehört. Doch an Stelle dieser Sonderbehandlung legen wir es einfach wieder mit unread() in den Datenstrom, und dann kann der nachfolgende Programmcode einfach so weitermachen, als ob nichts gewesen wäre. Dies ist besonders dann von Vorteil, wenn noch Unterprogramme im Einsatz sind, die nach dem Lesen der Zahl eine weitere Funktion aufrufen, die noch einmal alles lesen will. Nach der herkömmlichen Methode muss das gelesene Zeichen dann mit an die Funktion übergeben werden.

Listing 12.24   PushbackReaderDemo.java

import java.io.*;

class PushbackReaderDemo
{
  public static void main( String args[] ) throws IOException
  {
    String s = "134Erste Zeile\n234Zeile";

    PushbackReader in = new PushbackReader(new StringReader(s));

    boolean eof = false;
    int c;

    while ( !eof )
    {
      try
      {
        String number = "";

        // Lese Zahl bis nichts mehr geht

        while ( Character.isDigit((char)(c = in.read())) )
          number += (char)c;

        if ( c == -1 )   // Ende der Datei => Ende der Schleife
        {
          eof = true;
          continue;
        }
        else
          in.unread( c );        // Letztes Zeichen wieder rein

        System.out.print( Integer.parseInt(number) + ":" );

        // Hier ist das Zeichen wieder drinnen

        while ( (c = in.read()) != -1 )
        {
          System.out.print( (char)c );
          if ( c == '\n' )
            break;

        }
        if ( c == -1 ) {                 // Ende der Schleife
          eof = true;
          continue;
        }
      }
      catch ( EOFException e )
      {
        eof = true;
      }
    }
  }
}

Da PushbackReader nicht von BufferedReader abgeleitet ist und auch selbst keine readLine()-Methode anbietet, müssen wir mit einer kleinen Schleife selbst Zeilen lesen. Im Bedarfsfall muss die Zeichenkombination »\n\r« gelesen werden. So wie die Methode von uns jetzt programmiert ist, ist sie eingeschränkt auf Unix-Plattformen, die nur ein einziges Ende-Zeichen einfügen. Doch warum nutzen wie nicht readLine()? Wer nun auf die Idee kommt, folgende Zeilen zu schreiben, um doch in den Genuss der Methode readLine() zu kommen, ist natürlich auf dem Holzweg:

StringReader sr = new StringReader( s );
BufferedReader br = new BufferedReader ( sr );
PushbackReader in = new PushbackReader( br );
...
br.readLine();      // Achtung, br!!

Wenn wir dem PushbackReader das Zeichen wiedergeben, dann arbeitet der BufferedReader ja genau eine Ebene darüber und bekommt vom Zurückgeben nichts mit. Daher ist es sehr gefährlich, die Verkettung zu umgehen. Im konkreten Fall wird das unread() nicht durchgeführt und das erste Zeichen nach der Zahl fehlt.





Copyright © Galileo Press GmbH 2003
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