12.8 Schachteln von Eingabe-Streams
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.
12.8.1 Gepufferte Eingaben mit der Klasse BufferedReader
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
|
|
BufferedReader( Reader in )
Erzeugt einen puffernden Zeichenstrom mit der Puffergröße von 8 KB. |
|
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.
|
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 folgenden Zeilen:
BufferedReader 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 nachfolgende 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]) );
for ( String line; (line = in.readLine()) != null; )
System.out.println( line );
in.close();
}
}
catch ( IOException e ) {
System.err.println( e );
}
}
}
12.8.2 LineNumberReader zählt automatisch Zeilen mit
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.
Hier klicken, um das Bild zu Vergrößern
class java.io.LineNumberReader
extends BufferedReader
|
|
int getLineNumber()
Liefert die aktuelle Zeilennummer. |
|
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 gibt. 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
{
Reader fr = new FileReader( "LineNumberReaderDemo.java" );
LineNumberReader f = new LineNumberReader( fr );
for ( String line; (line = f.readLine()) != null; )
System.out.println( f.getLineNumber() + ": " + line );
f.close();
}
catch ( IOException e ) {
System.out.println( "Fehler beim Lesen der Datei" );
}
}
}
12.8.3 Eingaben filtern mit der Klasse FilterReader
Wie das Schachteln von Ausgabeströmen, so ist auch das Verbinden mehrerer Eingabeströme möglich. Als abstrakte Basiszwischenklasse 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.
Hier klicken, um das Bild zu Vergrößern
abstract class java.io.FilterReader
extends Reader
|
|
protected Reader in
Der Zeicheneingabestrom oder null, wenn der Strom geschlossen wurde. |
|
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 mit einem speziellen Filter überlesen
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 einfach nicht 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.
12.8.4 Daten mit der Klasse PushbackReader zurücklegen
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 Vorausschauender 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.
Hier klicken, um das Bild zu Vergrößern
class java.io.PushbackReader
extends FilterReader
|
|
PushbackReader( Reader in )
Erzeugt einen PushbackReader aus dem Reader in mit der Puffergröße 1. |
|
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.
|
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 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 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
{
int number = 0;
// Lese Zahl bis nichts mehr geht
while ( Character.isDigit((char)(c = in.read())) )
number = (number * 10) + (c-'0');
if ( c == -1 ) // Ende der Datei => Ende der Schleife
{
eof = true;
continue;
}
else
in.unread( c ); // Letztes Zeichen wieder rein
System.out.print( number + ":" );
// Hier ist das Zeichen wieder drinne
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 Methode readLine() 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 auf Unix-Plattformen eingeschränkt, die nur ein einziges Ende-Zeichen einfügen. Doch warum nutzen wir 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 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.
|