12.10 Die Filter für Zeichenströme
 
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.10.1 Gepufferte Eingaben mit der Klasse BufferedReader
 
Ein BufferedReader puffert ähnlich wie BufferedInputStream Daten. 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. |
Hinweis Eine Funktion readLine() gibt es auch in der Klasse DataInputStream, doch die Methode ist seit Java 1.1 veraltet und sollte nicht verwendet werden. Früher war Folgendes üblich, um eine Textzeile von der Konsole zu lesen:
DataInputStream in = new DataInputStream( System.in );
String s = in.readLine();
Mit der aktuellen Klasse ergibt sich jetzt:
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. Auf der Kommandozeile lassen sich Dateinamen übergeben. Die Dateiinhalte werden auf der Standardausgabe ausgegeben.
|
Listing 12.20
cat.java
import java.io.*;
class cat
{
public static void main( String[] args )
{
for ( String file : args )
{
BufferedReader in = null;
try
{
in = new BufferedReader( new FileReader(file) );
for ( String line; (line = in.readLine()) != null; )
System.out.println( line );
}
catch ( IOException e )
{
e.printStackTrace();
}
finally
{
if ( in != null )
try { in.close(); } catch ( IOException e ) { e.printStackTrace(); }
}
}
}
}
12.10.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 gelesenen Zeilen. 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, inkrementieren sie die Variable lineNumber.
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.
|
 Hier klicken, um das Bild zu Vergrößern
Listing 12.21
LineNumberReaderDemo.java
import java.io.*;
public class LineNumberReaderDemo
{
public static void main( String[] args )
{
Reader fr = null;
try
{
fr = new FileReader( "LineNumberReaderDemo.java" );
LineNumberReader lnr = new LineNumberReader( fr );
for ( String line; (line = lnr.readLine()) != null; )
System.out.println( lnr.getLineNumber() + ": " + line );
}
catch ( IOException e ) {
System.out.println( "Fehler beim Lesen der Datei" );
}
finally {
if ( fr != null )
try { fr.close(); } catch ( IOException e ) { e.printStackTrace(); }
}
}
}
12.10.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 das Argument in der protected-Variablen in. (Das ist das gleiche Prinzip wie bei den anderen FilterXXX-Klassen.) 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 1 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.22
HTMLReader.java
import java.io.*;
class HTMLReader extends FilterReader
{
private boolean intag = false;
public HTMLReader( Reader in )
{
super( in );
}
@Override
public int read() throws IOException
{
char[] buf = new char[1];
return read( buf, 0, 1 ) == –1 ? –1 : buf[ 0 ];
}
@Override 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;
}
}
public class HTMLReaderDemo
{
public static void main( String[] args )
{
try {
String s = "<html>Hallo!<b>Ganz schön fett.</b>"+
"Ah, wieder normal.</html>";
Reader sr = new StringReader( s );
Reader hr = new HTMLReader( sr );
BufferedReader in = new BufferedReader( hr );
for ( String t; (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.10.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, 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. Der nächste Lesezugriff liest dann nämlich 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.23
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) );
for ( int c; ; )
{
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 Eingabe? => Ende der Schleife
break;
in.unread( c ); // Letztes Zeichen wieder rein
System.out.print( number + ":" );
// Jetzt ist das Zeichen wieder drin
while ( (c = in.read()) != –1 )
{
System.out.print( (char)c );
if ( c == '\n' )
break;
}
if ( c == –1 )
break;
}
catch ( EOFException e )
{
break;
}
}
}
}
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.
Für die byteorientierten Stream-Klassen gibt es auch eine Klasse PushbackInputStream.
|