13 Ein- und Ausgabe in Java

Ein- und Ausgabeoperationen zählen zu den grundlegendsten Aktionen, die von einem Programm bewerkstelligt werden. Ob es nur um das Abspeichern und wieder Einlesen von Einstellungen geht oder gleich um ganze Dokumente von zig Seiten. Kaum ein Programm kommt noch ohne Ein- und Ausgabeoperationen aus.

Ein- und vor allem Ausgabe bedeutet normalerweise, dass Daten aus einer Datei gelesen oder in eine Datei geschrieben werden. Innerhalb von Applets ist dies meist durch die Einstellungen des Containers grundsätzlich verboten. Die Klassen zur Ein- und Ausgabe sind also normalerweise eher für echte Java-Applikationen als für Java-Applets gedacht.

In Java werden diese Ein- und Ausgabeoperationen mittels so genannter Datenströme realisiert. Der Begriff »Strom« geht auf Unix zurück - das Pipe-Betriebssystem. Unter einer Pipe versteht man einen nicht-interpretierten Strom von Bytes. Er wird zur Kommunikation von Programmen untereinander bzw. von Programmen und Hardwareschnittstellen verwendet. >

Zum Thema Ausgabe zählt ebenfalls das Drucken unter Java. Dies war in den vergangenen Versionen von Java ein gewisses Problem. Unter dem SDK 2 mit dem JDK 1.3 ist die Programmierung von Druckoperationen jedoch relativ unkompliziert.

13.1 Allgemeines zur Ein- und Ausgabe unter Java

Ein Datenstrom kann von jeder beliebigen Quelle herkommen, d.h., der Ursprungsort spielt überhaupt keine Rolle, ob Internet, lokaler Server oder lokaler Rechner ist egal. Dies mag im ersten Moment als nicht sonderlich wichtig erscheinen, ist jedoch von entscheidender Bedeutung.

Nehmen wir zur Verdeutlichung einfach mal ein anderes Abstraktionsmodell. Hier muss sich der Empfänger von Daten selbst um die Abholung kümmern. Der Strom fließt also nicht einfach an ihm vorbei. Eine genaue Kenntnis des Quellortes und diverser weiterer Informationen der Quelle (Zugriffsmöglichkeiten) müssen erst beim Empfänger vorhanden sein, bevor er die Daten abholen kann, die ihn eigentlich interessieren.

In unserem Modell des Datenstroms hingegen steckt die Information über die Quelle in einem Strom-Argument. Außerdem gibt es noch ein weiteres Argument, in das die verarbeiteten Daten zurückgeschrieben werden können.

Das Thema selbst ist sehr abstrakt und umfangreich, denn Java bietet eine Vielzahl von unterschiedlichen Strömen, die wir hier der Vollständigkeit halber ansprechen wollen. Wir werden uns aber darauf beschränken, die für Sie wahrscheinlich wichtigsten Ströme (die zur Ein- und Ausgabe auf einem Dateisystem) mit Beispielen genauer zu vertiefen.

Unter Java stehen zur Behandlung des Datenstrommodells im Wesentlichen zwei abstrakte Klassen zur Verfügung:

InputStream
OutputStream

Die beiden Klassen gehören zu dem Paket java.io, das Sie bei jedem Programm mit Ein- oder Ausgabeoperationen importieren sollten. Das Paket java.io enthält eine relativ große und hierarchisch gut durchstrukturierte Anzahl von Klassen und Schnittstellen zur Unterstützung von Ein- und Ausgabe. Die meisten dieser Klassen leiten sich von den besagten abstrakten Klassen InputStream und OutputStream ab bzw. verwenden sie als Argumente. Es gibt aber auch Klassen, die nicht auf diese beiden abstrakten Klassen zurückgehen.

Eine der wohl wichtigsten Nicht-Streamklassen ist die Klasse File, die Dateinamen und Verzeichnisnamen in einer plattformunabhängigen Weise verwaltet. Die Klasse File bietet Dienste an, um auf lokalen Systemen Dateien und Verzeichnisse aufzulisten, Dateiattribute zu erfragen und Dateinamen zu ändern oder zu löschen. Mit der Schnittstelle FilenameFilter ist es möglich, Klassen zu implementieren, die einen Filter für die Dateiverwaltung erzeugen, also zum Beispiel nur das Lesen von »*.doc«-Dateien. Diese Schnittstelle kann beispielsweise innerhalb der Klasse java.awt.FileDialog verwendet werden, um die Anzeige von Dateien einzuschränken. Über die Klasse RandomAccessFile lassen sich Zeichen an willkürlichen Positionen innerhalb von Dateien auslesen. Diese Klasse wird aber wohl eher weniger verwendet, da für das Einlesen und Auslesen von Daten aus einer Datei die Verwendung einer Streamklasse weitaus sinnvoller ist.

Zwar verfügen einige Ströme über ein paar zusätzliche Methoden, aber dennoch gibt es bestimmte Methoden, die immer zur Verfügung stehen.

Die einfachen Strom-Methoden erlauben nur das Versenden von Bytes mittels Datenströmen. Zum Senden verschiedener Datentypen gibt es die Schnittstellen DataInput und DataOutput. Sie legen Methoden zum Senden und Empfangen anderer Java-Datentypen fest. Mithilfe der Schnittstellen ObjectInput und ObjectOutput lassen sich ganze Objekte über einen Strom zu senden. Mit dem StreamTokenizer können Sie einen Strom wie eine Gruppe von Worten behandeln. Er ist dem StringTokenizer ähnlich, der das Gleiche mit Zeichenketten tut.

Eine weiter Streamklasse - StringBufferInputStream - liest Daten aus dem StringBuffer und ermöglicht damit eine Cache-Funktionalität.

Für die Kommunikation von einzelnen Threads gibt es die beiden Klassen PipedInputStream zum Lesen von Daten aus einem PipedOutputStream. Der PipedOutputStream dient also zum Schreiben in einen PipedInputStream.

Alle Methoden, die sich mit Eingabe- und Ausgabeoperationen beschäftigen, werden in der Regel mit throws IOException abgesichert. Diese Subklasse von Exception enthält alle potenziellen I/O-Fehler, die bei der Verwendung von Datenströmen auftreten können.

Von IOException abgeleitete Exceptions können (und müssen) direkt mit einem try-catch-Block aufgefangen, oder an eine übergeordnete Methode weitergegeben werden.

Beginnen wir gleich mit einem kleinen Beispiel, obwohl uns noch diverse Details fehlen, die erst im Folgenden besprochen werden. Das Beispiel ist dennoch recht einfach zu verstehen. Es handelt sich um ein Programm, das eine als Aufrufparameter an das Programm übergegebene Datei ausliest und auf dem Bildschirm ausgibt. Am Ende wird dann noch die Größe der Datei angezeigt.

import java.io.*;
public class LeseDat {
int i;
public void lese(String quelldatei) {
// Erstellen eines Eingabestroms 
try {
 DataInput quelle = new DataInputStream(new FileInputStream(quelldatei));
 while(true) {
// Einlesen eines byte aus der Quelle
   byte zeichen = quelle.readByte();
   System.out.print((char)zeichen);
   i++;    
  }  }
// Dateiende der Quelldatei erreicht.
// Explizite Verwendung einer Exception um
// die while()-Schleife abzubrechen
catch (EOFException e)  {
System.out.println(
"Alles klar! Lesen der Datei beendet.");
System.out.println("Dateigroesse in Byte: " + i);  }
catch (IOException e)  // allgemeiner IO-Fehler
{
System.out.println(e.getMessage());  }  }
public static void main(String[] args) {
LeseDat a = new LeseDat();
try {
  a.lese(args[0]);
}
catch(ArrayIndexOutOfBoundsException e) {
 System.out.println(e.toString());
 System.out.println("Bitte eine Datei als Parameter angeben");
}  }
}

Wenn man als einzulesende Datei den Quellcode des Programms selbst verwendet, wird das Ergebnis dieses Programms so aussehen:

Abbildung 13.1:  Die Ausgabe des Beispiels bei einer korrekt angegebenen Datei

Wenn ein Parameter vergessen wird, erfolgt die entsprechende selbst erstellte Meldung in dem catch-Block. Wenn die Datei nicht gefunden wird, wird die Meldung vom System generiert.

Abbildung 13.2:  Datei nicht gefunden

Das Kapitel beinhaltet zwar diverse Beispiele, aber zahlreiche spezielle Ein- und Ausgabeoperationen werden wir in anderen Kapiteln (besonders in dem zu den erweiterten Java-Techniken) im Zusammenhang mit den dazu notwendigen ergänzenden Techniken in der Praxis zeigen.

13.2 Die Klasse InputStream

Diese Klasse ist Basis für viele der wichtigsten Leseoperationen eines Bytestroms. Woher die Bytes kommen und wie sie befördert werden, spielt keine Rolle, sie müssen dem einlesenden Objekt nur zur Verfügung stehen.

13.2.1 Methoden zum Einlesen

Zum Einlesen von Bytes aus Datenströmen dient hauptsächlich die Methode read(), von der es diverse Variationen und spezialisierte Erweiterungen gibt.

Allen read()-Methoden ist gemeinsam, dass sie auf Beendigung aller angeforderten Eingaben warten. Deshalb packt man Eingabeoperationen auch meist in einen extra Thread.

Die einfachste Form ist public abstract int read() throws IOException. Diese liest ein einzelnes Byte aus dem Eingabestrom und gibt es aus. Wenn der Strom das Dateiende erreicht, gibt diese Methode -1 aus.

Die Rückgabe -1 bedeutet im Unterschied zu C nicht, dass ein Fehler eingetreten ist. Fehler werden immer als IOException ausgeworfen.

Etwas komfortabler ist die folgende read()-Variante:

public int read(byte[] buffer) throws IOException

Hier wird ein Puffer (ein Datenfeld) mit Bytes gefüllt, die aus einem Strom gelesen wurden. Die Methode gibt die Anzahl der gelesenen Bytes zurück. Diese Variante der read()-Methode kann bei Bedarf weniger Bytes lesen als das Datenfeld aufnehmen kann. Dies kann dann geschehen, wenn im Datenstrom nicht genug Bytes vorhanden sind, um das Feld vollständig zu füllen. In dem Fall gibt die Methode die Anzahl der gelesenen Bytes zurück. Wenn der Strom das Dateiende erreicht, gibt diese Methode wie die erste Variante -1 aus.

Beispiel:

InputStream datenstrom = methodezumLeseneinerDatenquelle()
byte[] buffer = new byte[42]; 
if (datenstrom.read(buffer)  != buffer.length) {
  ...tue irgendwas mit den eingelesenen Daten...
}

Sie können mit einer weiteren Variante von read() gezielt in einem Bereich ihres Puffers lesen:

public int read(byte[] buffer, int beginn, int laenge) throws IOException

Damit wird ein Puffer beginnend mit Position beginn mit der mit laenge angegebenen Anzahl von Bytes aus dem Strom gefüllt. Es wird entweder die Anzahl der gelesenen Bytes oder -1 für das Dateiende ausgegeben.

Um sämtliche Varianten der read()-Methode zu sehen, sei explizit auf die Online- Dokumentation verwiesen.

13.2.2 Blockaden vorher abfragen

Da allen read()-Methoden gemeinsam ist, dass sie auf Beendigung aller angeforderten Eingaben warten, kann es - gerade in Netzwerken - zu längeren Blockaden beim Einlesen von Daten kommen. Die read()-Methode ist unter Umständen blockiert und wartet auf Daten, ohne etwas zurückzugeben, wenn keine Daten verfügbar sind. Zwar erlaubt Multithreading, dass andere Operationen des Programms parallel ablaufen. Dennoch möchte man eventuelle Blockaden oft vermeiden. Dazu können Sie vorzeitig anfragen, wie viele Bytes ohne Blockieren gelesen werden können. Zu diesem Zweck haben Sie die Methode public int available() throws IOException zur Verfügung. Sie gibt diese Anzahl zurück:

Die Methode available() arbeitet in vielen Fällen aber nicht perfekt, was in der Natur der Datenströme liegt. Einige Datenströme geben beispielsweise immer 0 aus. Sie sollten sich also nicht auf die Methode verlassen. Sie können sich zwar weitgehend darauf verlassen, dass die angegebene Anzahl von Bytes, die Ihnen mitgeteilt wird, auch ohne Blockierung gelesen werden kann (dementsprechend als untere Grenze zu verstehen). Es können aber ebenso mehr sein, was bei einige Datenströmen oft der Fall ist (als obere Grenze infolgedessen unzuverlässig).

13.2.3 Überspringen von Daten

Für den Fall, dass Sie Daten in einem Strom überspringen wollen, steht Ihnen die Methode public long skip(long n) zur Verfügung. Indem Sie die Anzahl von Bytes (in Form eines long-Datentyps) angeben, die Sie überspringen wollen, beginnen Sie erst an der darauffolgenden Stelle mit der Leseoperation. Die skip()-Methode benutzt implizit die read()-Methode, um die angegebene Anzahl von Bytes zu überspringen. Sie gibt die Anzahl der Bytes aus, die sie übersprungen hat, oder -1, wenn sie am Ende der Datei angelangt ist.

13.2.4 Positionen beim Lesen markieren

Einige Eingabeströme unterstützen eine Markierung einer Position im Strom und ein folgendes Zurücksetzen des Stroms an diese Position. Sie können sich wie eine Art Lesezeichen verhalten.

Der Strom muss sich gewissermaßen an alle Bytes, die ihn durchfließen, erinnern. Es muss betont werden, dass nur einige Ströme diese Technik unterstützen und auch bei diesen nicht an jede Stelle ein Lesezeichen gesetzt werden kann, sondern nur an gewissen Stützpunkten in festgelegten Abständen. Wir werden solche Stromtypen noch explizit kennen lernen. Hier behandeln wir erst einmal die Methode, mit der Sie untersuchen können, ob der Strom Markieren unterstützt.

Die Methode public boolean markSupported gibt true aus, wenn der Strom Markieren unterstützt.

Wenn ein Strom Markieren unterstützt, können Sie die folgende Methode für die konkrete Markierung verwenden:

public synchronized void mark(int leseLimit)

Der Parameter leseLimit legt die maximale Anzahl Bytes fest, die vor dem Zurücksetzen weitergegeben werden soll. Das Zurücksetzen auf die Markierung selbst erfolgt mit der Methode public synchronized void reset() throws IOException.

Beispiel:

// Datenstrom bekommt beliebige Daten
// Datenstrom unterstützt Markieren
if (datenstrom.markSupported()) {
  ...// lese einen Teil der Daten...
datenstrom.mark(42);  
...// lese maximal 42 Bytes
datenstrom.reset();  
...// Zurücksetzen und die letzten Bytes erneut lesen
}
else // Datenstrom unterstützt kein Markieren
{
...// tue was anderes
}

13.2.5 Ressourcen freigeben

Wenn Sie mit der Verarbeitung von einem Strom fertig sind, sollten Sie ihn mit der Methode public void close() throws IOException wieder explizit schließen.

Diese Vorgehensweise ist zwar nicht unbedingt zwingend, denn die meisten Ströme werden automatisch bei der Garbage Collection oder mit einer geeigneten finalize()-Methode geschlossen. Es gibt aber einige Gründe, warum man es dennoch tun sollte. Es kann durchaus vorkommen, dass Sie den Strom wieder öffnen wollen, bevor die automatische Bereinigung stattgefunden hat, um daraus zu lesen. Außerdem ist in den meisten Betriebssystemen die Zahl der Dateien, die gleichzeitig geöffnet sein können, begrenzt. Am besten schließt man einen Strom immer in einem try-catch-finally-Konstrukt.

Beispiel:

InputStream datenstrom = methodezumLeseneinerDatenquelle()
try {
...// versuchen, Daten zu lesen und zu verarbeiten
}
finally {
datenstrom.close();
}

13.3 Die Klasse OutputStream

Anders als der Eingabestrom, der eine Datenquelle darstellt, ist der Ausgabestrom ein Empfänger für Daten. Man findet Ausgabeströme fast nur in Verbindung mit Eingabeströmen. Führt ein InputStream eine Operation aus, wird die zugehörige umgekehrte Operation vom OutputStream durchgeführt. Mit dieser abstrakten Klasse können viele der wichtigsten Schreiboperationen eines Bytestroms verwirklicht werden. Die Identität der Bytes und wie sie befördert werden spielt wie bei der abstrakten Klasse InputStream keine Rolle.

13.3.1 Methoden zum Schreiben

Die grundlegendste Methode eines OutputStream-Objekts ist die write()-Methode zum Erzeugung eines Ausgabestroms. Sie gibt es wie die read()-Methode in diversen Variationen.

Allen write()-Methoden ist wie den read()-Methoden gemeinsam, dass sie auf Beendigung des vollständigen Vorgangs (in diesem Fall des Schreibvorgangs) warten. Deshalb packt man auch Schreiboperationen meist in einen extra Thread.

Die einfachste Version public abstract void write(int b) throws IOException schreibt ein einzelnes Zeichen in einen Ausgabestrom. Etwas praxisorientierter ist die folgende Variation:

public void write(byte[] buffers) throws IOException

Sie schreibt den gesamten Inhalt des Datenfeldes buffers in den Ausgabestrom. Dazu sollte der Puffer natürlich vorher mit Bytes gefüllt werden.

Es gibt natürlich auch ein schreibendes Gegenstück zu der Variante von read(), die gezielt in einem Bereich des Puffers liest. Die Methode public void write(byte[] buffer, int beginn, int laenge) throws IOException schreibt einen Puffer beginnend mit Position beginn bis zur mit laenge angegebenen Anzahl von Bytes in den Ausgabestrom.

Für sämtliche read()-Methoden werden Sie in der gleichen Klasse bzw. Schnittstelle eine passende write()-Methode finden. Mehr zu den Details finden Sie in der Online-Dokumentation.

Spielen wir auch hier ein komplettes Beispiel durch, das einige der erklärten Techniken in der Praxis zeigt. Das Programm liest die als ersten Parameter angegebene Datei aus und schreibt die Daten in die als zweiten Parameter angegebene Datei. Dabei ist es gegen Fehlbedienung über eine Ausnahmebehandlung abgesichert. Es handelt sich also um eine Nachprogrammierung eines Kopierbefehls - allerdings vollkommen plattformneutral!

/* Ein Kopierprogramm, das eine Datei Zeichen
für Zeichen liest und dann in eine Zieldatei
schreibt.
*/
import java.io.*;
public class KopiereDatei {
/* Die Kopiermethode. Als Übergabeargumente 
werden die Quelldatei und die zu erstellende 
Zieldatei verwendet. Diese Parameter müssem 
beim Aufruf angegeben werden.
Das wird über das Exception-Handling direkt in der
main()-Methode sichergestellt. */
public static void kopiere(String quelldatei, String zieldatei) throws IOException {
// Erstellen eines Eingabestroms 
// und eines Ausgabestroms
DataInput quelle = new DataInputStream(new FileInputStream(quelldatei));
DataOutput ziel = new DataOutputStream(new FileOutputStream(zieldatei));
try {
 while(true) {
// Einlesen eines Char aus der Quelle
   char zeichen = quelle.readChar();  
   // Schreibe in Zieldatei
   ziel.writeChar(zeichen); 
  }  }
// Dateiende der Quelldatei erreicht.
catch (EOFException e)  {
System.out.println(
"Alles klar! Kopieren der Datei beendet.");
}
catch (IOException e) { // allgemeiner IO-Fehler
System.out.println(e.getMessage());
}  }
public static void main (String args[]) throws IOException {
// Fange fehlende Übergabeparameter ab
 try {
 kopiere(args[0], args[1]);  
 }
 catch (ArrayIndexOutOfBoundsException uebergabeEx) { // Keine Übergabeparameter.
   System.out.println("Das Programm benoetigt zwei Uebergabeparameter.");
   System.out.println(
"Sie muessen folgende Syntax eingeben:");
   System.out.println("java KopiereDatei [Name der Quelldatei] [Name der Zieldatei]");
 }  }
}

13.3.2 Den gepufferten Cache ausgeben

Abhängig vom Datenstrom müssen Sie den Datenstrom gelegentlich leeren, wenn Sie sicher gehen wollen, dass die Daten, die Sie in den Strom geschrieben haben, angekommen sind. Das Leeren eines Stroms zerstört keine Informationen im Strom, es soll nur sicherstellen, dass alle Daten, die in internen Puffern gespeichert sind, an diejenige Stelle geschrieben werden, an die der Strom gerade angebunden ist. Um einen Ausgabestrom zu leeren, müssen Sie einfach nur die Methode public void flush() throws IOException aufrufen.

13.3.3 Ressourcen freigeben

Wenn Sie mit einem Strom fertig sind, sollten Sie genau wie bei Eingabeströmen auch die Ausgabeströme mit der Methode public void close() throws IOException wieder explizit schließen, was Sie so auch in den im Kapitel verwendeten Beispielen sehen.

Diese Vorgehensweise ist wieder nicht unbedingt zwingend, denn die meisten Ströme werden automatisch bei der Garbage Collection oder mit einer geeigneten finalize()-Methode geschlossen. Es gelten jedoch die gleichen Gründe wie bei Eingabeströmen, warum man es dennoch tun sollte.

13.4 Byte-Datenfeldströme

Man muss nicht unbedingt in eine Datei oder in das Netzwerk schreiben, um Ströme zu benutzen. Sie können auch unter Verwendung der ByteArrayInputStream- und ByteArrayOutputStream-Klassen Datenfelder aus Bytes lesen und schreiben. Für Byte-Datenfeldströme stehen die gleichen Standardmethoden wie bei allen Datenströmen zur Verfügung.

Es handelt sich bei der Verwendung der ByteArrayInputStream-Technik um ein Beispiel für die explizite Erstellung von Eingabeströmen. Sie nutzen also nicht einen Eingabestrom, der einfach schon »da« ist, sondern Sie müssen ein Datenfeld mit Bytes haben, das als Byte-Quelle dient, die vom Strom gelesen wird.

public ByteArrayInputStream(byte[] buffer) erstellt einen Byte-Eingabestrom durch die Verwendung des gesamten Inhalts der Bytes als Daten im Strom.

Die Methode public ByteArrayInputStream(byte[] buffer, int beginn, int laenge) erstellt einen Byte-Eingabestrom der bis zu laenge Bytes liest, angefangen mit der Position beginn.

Beispiel:

eingabeStrom meinStrom = new ByteArrayInputStream(byte[] buffer, 100, 200);

In dem Beispiel ist der Strom 200 Byte lang und enthält die Bytes 100 bis 299 aus dem Array buffer.

Das Gegenstück zu dem ByteArrayInputStream ist ein ByteArrayOutputStream, ein Datenfeld mit Bytes. Der ByteArrayOutputStream gibt Bytes kontinuierlich an einen Puffer weiter, die darin gespeichert werden. Der Konstruktor der ByteArrayOutputStream-Klasse nimmt einen optionalen Parameter für die Anfangsgröße, der die Anfangsgröße des Datenfeldes festlegt, das die in den Strom geschriebenen Bytes speichert:

public ByteArrayOutputStream() 
public ByteArrayOutputStream(int initialSize)

Nachdem Sie Daten in einen ByteArrayOutputStream geschrieben haben, können Sie ihn auf verschiedene Art weiter verwenden. Sie können beispielsweise den Inhalt des Stroms zu einem Datenfeld mit Bytes konvertieren, indem Sie die Methode public synchronized byte[] toByteArray() aufrufen.

Eine andere Verwendung ist die Weitergabe an einen anderen Datenstrom mittels der Methode public void writeTo(OutputStream out) throws IOException.

Die Methode public String toString() konvertiert den Datenstrom (wie bei allen Objekten) in eine Zeichenkette.

Die ByteArrayOutputStream-Klasse besitzt mit der size()-Methode eine Möglichkeit zur Bestimmung der Anzahl an Bytes, die bis dahin in den Strom geschrieben wurden. Die reset()-Methode setzt das Array an den Anfang zurück.

13.5 Der StringBufferInputStream

Der StringBufferInputStream ist dem ByteArrayInputStream sehr ähnlich. Der einzige Unterschied zwischen den beiden liegt darin, dass der Konstruktor für den StringBufferInputStream als Quelle eine Zeichenkette anstelle eines Datenfeldes mit Bytes verwendet:

public StringBufferInputStream(String zeichenkette)

Beispiel:

String buffer = "Die Antwort ist ... 42";
InputStream meinEingabeStrom = new StringBufferInputStream(buffer);

13.6 Gefilterte Ströme

Die Technik der gefilterten Eingabe-Ströme bietet alle Methoden der normalen InputStream-Klasse. Eine der wichtigsten Eigenschaften von gefilterten Strömen ist die Möglichkeit des Anhängens von Strömen an das Ende eines anderen Stroms. Das bedeutet, dass die Ströme verschachtelt werden und der Filter (sinnvollerweise die äußere Verschachtelungsebene) nur die Daten des inneren Stroms durchlässt, die im Filter eingestellt sind.

Der einfache Eingabestrom hat zum Beispiel nur die read()-Methode zum Lesen von Bytes. Wenn Sie Zeichenketten und Zahlen lesen wollen, können Sie einen speziellen gefilterten Dateneingabestrom mit dem Eingabestrom verbinden. Damit haben Sie Methoden zur Verfügung, mit denen Sie Zeichenketten, ganze Zahlen und sogar Gleitkommazahlen lesen und sie nach Typ trennen können.

Die Klassen FilterInputStream und FilterOutputStream ermöglichen das Verknüpfen von Strömen. Allerdings stellen sie keine neuen Methoden bereit. Ihren Nutzen erzielen sie einfach daraus, dass sie mit einem anderen Strom verbunden sind. Die Konstruktoren für den FilterInputStream und Filter-OutputStream haben deshalb InputStream- und OutputStream-Objekte als Parameter:

public FilterInputStream(InputStream in) 
public FilterOutputStream(OutputStream out)

Da diese Klassen selbst Instanzen von InputStream und OutputStream sind, können Sie als Parameter für Konstruktoren anderer Filter dienen, was die Konstruktion langer Ketten von Ein- und Ausgabefiltern ermöglicht.

Beispiel:

meinEingabeStrom2 = new FilterInputStream(new FilterInputStream(meinEingabeStrom1));

Eine sehr wichtige Subklasse von FilterInputStream ist BufferedInputStream.

13.7 Gepufferte Ströme

Gepufferte Ströme fungieren als Cache für Lese- und Schreibvorgänge und tragen damit in vielen Fällen zur Beschleunigung eines Programms bei. Statt in vielen kleinen Schreib- oder Leseoperationen werden die Bytes in großen Blöcken gesammelt und dann auch bei den Schreib- oder Leseoperationen als großer Block behandelt. Gepufferte Ströme implementieren die vollen Fähigkeiten der normalen InputStream/OutputStream-Methoden. Sie unterstützen hervorragend das Markieren von Stellen im Strom (die Methoden mark() und reset()).

Gepufferte Ströme werden mit den Klassen BufferedInputStream für die Eingabe und BufferedOutputStream für die Ausgabe realisiert. Wenn Sie sie erstellen, können Sie eine Puffergröße (buffergroesse) angeben:

public BufferedInputStream(InputStream in) 
public BufferedInputStream(InputStream in, int buffergroesse) 
public BufferedOutputStream(OutputStream out) 
public BufferedOutputStream(OutputStream out, int buffergroesse)

Die BufferedInputStream-Klasse versucht in einem einzigen read()-Auf- ruf so viele Daten wie möglich in ihren Puffer einzulesen. Die BufferedOutputStream-Klasse ruft nur dann die write()-Methode auf, wenn ihr Puffer voll ist oder wenn die flush()-Methode aufgerufen wird.

Sie können analog zu gefilterten Strömen andere Ströme mit gepufferten Strömen verbinden, um diese damit in gewisser Weise »zu filtern«. Auch eine Kombination mit gefilterten Strömen ist möglich und sehr oft sinnvoll.

Beispiel:

meinEingabeStrom2 = new BufferedInputStream(new FilterInputStream(meinEingabeStrom1));

13.8 Datenströme

Die Filterklassen DataInputStream und DataOutputStream sind zwei der nützlichsten Filter des java.io-Paketes. Sie ermöglichen es, primitive Typen in Java auf maschinenunabhängige Art und Weise zu lesen und zu schreiben. Die Klassen DataInputStream und DataOutputStream kümmern sich selbstständig um die notwendigen Konvertierungen.

Alle Methoden dieser Klassen sind in zwei separaten Schnittstellen definiert, die sowohl von DataInputStream bzw. DataOutputStream als auch von einer weiteren Klasse des java.io-Pakets - RandomAccessFile - implementiert wird. Die Schnittstellen sind so allgemein, dass sie im Prinzip von jeder Klasse benutzt werden können. Es handelt sich um die DataInput-Schnittstelle bzw. DataOutput-Schnittstelle.

13.8.1 Die DataInput-Schnittstelle

Normalerweise bieten Byteströme kein Format. Damit haben Sie Probleme, wenn Sie primitive Datentypen direkt einlesen wollen. Die Klasse DataInputStream implementiert deshalb eine DataInput-Schnittstelle, die Methoden zum Lesen von primitiven Datentypen in Java definiert. Zusätzlich gibt es noch ein paar weitere Methoden.

Die ausgeworfenen Ausnahmen sind vom Typ IOException oder EOFException. IOException gilt für alle read()-Methoden, EOFException für fast alle (außer readLine(), readUTF() und skipBytes()). Zu EOFException gibt es noch eine nützliche Bemerkung: EOFException wird ausgeworfen, wenn das Ende des Stroms erreicht ist. Diese Ausnahme lässt sich überall da einsetzen, wo bisher auf -1 überprüft wurde. Damit können viele Sourcecodes übersichtlicher gestaltet werden. Wir werden die Ausnahme gleich in einem Beispiel einsetzen.

Die DataInput-Methoden zum Lesen primitiver Datentypen sind folgende:

public boolean readBoolean()  
public byte readByte() 
public byte readUnsignedByte() 
public char readChar()  
public short readShort()  
public short readUnsignedShort()  
public int readInt()  
public long readLong()  
public float readFloat()  
public double readDouble()  
public String readUTF()

Es fällt wahrscheinlich auf, dass die Methode zum Einlesen von Zeichenketten readUTF() heißt. UTF bedeutet Unicode Transmission Format und ist das spezielle Format zum Kodieren von 16-Bit-Unicode-Werten, was unter Java verwendet wird. ASCII-Code kann aber genauso damit gelesen werden.

Die beiden mit »Unsigned« bezeichneten Methoden arbeiten wie ihr Gegenstück ohne diesen Zusatz, können jedoch zu einer effizienteren Verwendung der Bits in einem Bytestrom eingesetzt werden, wenn das Vorzeichen nicht von Interesse ist.

Wenn Sie Daten aus einer Textdatei lesen, wird meistens eine Zeile durch eine Zeilenschaltung begrenzt. Zum Lesen einer so begrenzten Zeile gibt es die Methode public String readLine() throws IOException. Sie ließt in einer Zeile einer Textdatei, die durch \r, \n oder das Ende der Datei terminiert wird, wobei das \r, \n oder \r\n entfernt wird, bevor die Zeile als Zeichenkette wiedergegeben wird.

Wenn Sie versuchen, mit der normalen read()-Methode aus der InputStream-Klasse eine fixe Anzahl an Bytes in einem Datenfeld zu lesen, kann es vorkommen, dass Sie diese mehrmals aufrufen müssen. Dies hat in der Regel zur Folge, dass bereits eine Ausgabe beginnt, bevor alle angeforderten Bytes gelesen wurden. Denken Sie nur an den Datentransfer über ein Netzwerk wie das Internet. Wenn Sie das explizit nicht wünschen, hilft Ihnen die readFully()-Methode, die es in zwei Ausprägungen gibt. Diese wartet explizit auf alle Bytes, die Sie verlangt haben:

public void readFully(byte[] buffer) throws IOException 
public void readFully(byte[] buffer, int beginn, int länge) throws IOException

Die Methode public int skipBytes(int anzahlBytes) erfüllt eine ähnliche Funktion wie die readFully()-Methode. Sie wartet, bis die gewünschte Anzahl Bytes übersprungen wurde, bevor sie zurückkehrt.

13.8.2 Die DataOutput-Schnittstelle

Die DataOutput-Schnittstelle ist das Gegenstück zu DataInput-Schnittstelle und definiert die Ausgabemethoden, die den Eingabemethoden entsprechen, die dort definiert wurden. Die durch diese Schnittstelle definierten Methoden sind (alle werfen eine IOException aus):

public void writeBoolean(boolean b)  
public void writeByte(int b)  
public void writeChar(int c)  
public void writeShort(int c)  
public void writeInt(int i)  
public void writeLong(long l)  
public void writeFloat(float f)  
public void writeDouble(double d)  
public void writeUTF(String s)

Die vorzeichenlosen Lesemethoden (Unsigned) haben kein direktes Gegenstück.

Durch die Verwendung der Methoden public void writeBytes(String s) throws IOException und public void writeChars(String s) throws IOException können Sie eine Zeichenkette als eine Reihe von Bytes oder Zeichen schreiben.

Die Schnittstelle definiert ebenso die vorher schon beschriebenen folgenden Methoden:

public abstract void write(int b) throws IOException 
public void write(byte[] buffers) throws IOException 
public void write(byte[] buffer, int beginn, int laenge) throws IOException.

13.8.3 Die DataInputStream- und DataOutputStream-Klassen

Bei den Klassen DataInputStream und DataOutputStream handelt es sich einfach um Stromfilter, die die DataInput- und DataOutput-Schnittstellen implementieren. Ihre Konstruktoren sind typische Stromfilter-Konstruktoren, da sie einfach den zu filternden Strom als Parameter verwenden:

public DataInputStream(InputStream in) 
public DataOutputStream(OutputStream out)

13.9 Die PrintStream-Klasse

Methoden der PrintStream-Klasse haben wir schon benutzt, ohne es direkt so zu bezeichnen. Der System.out-Strom ist beispielsweise eine Instanz von PrintStream. Die dazu gehörenden Methoden System.out.print() und System.out.println() kennen wir.

Die PrintStream-Klasse ermöglicht im Allgemeinen das Schreiben ausgebbarer Versionen verschiedener Objekte in einen Ausgabestrom. Dabei verwenden Sie die Variable out der System-Klasse. System.err gehört ebenfalls zu der PrintStream-Klasse, System.in ist ein InputStream.

Wenn Sie ein PrintStream-Objekt erstellen, müssen Sie es an einen bereits existierenden Augabestrom hängen, da es sich um einen FilterOutputStream handelt. Sie können einen optionalen Parameter zum automatischen Leeren angeben, der, falls true, den Strom automatisch dazu bringt, immer die flush()-Methode aufzurufen, wenn sie eine neue Zeile ausgibt:

public PrintStream(OutputStream out) 
public PrintStream(OutputStream out, boolean autoFlush)

Die Methode flush() wird genauso wie close() und write() in PrintStream implementiert. Dazu gibt es eine Fülle von Möglichkeiten zur Ausgabe von Primitivtypen. Die PrintStream-Klasse besitzt für die Ausgabe von Objekten folgende Methoden:

public void flush() 
public void close()
public abstract void write(int b) 
public void write(byte[] buffer, int beginn, int laenge) 
public void print(Object o) 
public void print(String s) 
public void print(char[] buffer) 
public void print(boolean b) 
public void print(char c) 
public void print(int i) 
public void print(long l) 
public void print(float f) 
public void print(double d) 
public void println(Object o) 
public void println(String s) 
public void println(char[] buffer) 
public void println(boolean b) 
public void println(char c) 
public void println(int i) 
public void println(long l) 
public void println(float f) 
public void println(double d) 
public void println()

Der einzige Unterschied zwischen den Methoden print() und println() ist der, dass die println()-Methode bei der Ausgabe immer einen Zeilenvorschub erzeugt, die reine print()-Methode nicht. Die println()-Methode ohne Parameter gibt einfach eine neue Zeile aus.

13.10 Pipe-Ströme

Die PipedInputStream und PipedOutputStream-Klassen ermöglichen Ihnen einen Eingabestrom direkt mit einem Ausgabestrom zu verbinden. Wenn Sie es normalerweise mit Strömen zu tun haben, ist die Quelle oder der Bestimmungsort von Daten eine externe Datei oder das Netzwerk. Wenn Sie Pipes verwenden, ist Ihr Programm sowohl Quelle als auch Empfangsort von Daten. Die Technik ist Unix-entlehnt und schafft eine direkte Verbindung von Strömen, aus denen ein und dasselbe Programm sowohl liest als auch Daten in sie schreibt. Das wird im Wesentlichen für die Kommunikation zwischen Threads verwendet. Eine andere denkbare Anwendung ist das Testen beider Enden eines Netzwerk-Protokolls mit einem einzelnen Programm. Wem noch das Pipe-Symbol aus DOS bekannt ist, wird vermuten, wozu diese Technik noch dienen kann.

Es mag trivial erscheinen, ist jedoch in diesem Fall so wichtig, dass es noch einmal extra erwähnt werden soll: Bei der Verwendung mit verschiedenen Threads ist ein sorgfältige Synchronisation unabdingbar.

Wenn Sie einen PipedInputStream oder PipedOutputStream erstellen, können Sie den Strom angeben, mit dem er verbunden werden soll. Wenn Sie im Konstruktor keinen Strom angeben, ist der Strom nicht verbunden und muss erst mit irgendeinem Strom verbunden werden, bevor er benutzt werden kann. Die Konstruktoren für PipedInputStream und PipedOutputStream sind folgende:

public PipedInputStream() 
public PipedInputStream(PipedOutputStream ausgabeStream) 
public PipedOutputStream() 
public PipedOutputStream(PipedInputStream eingabeStream)

Die Methode public void connect(PipedInputStream eingabeStream) throws IOException in PipedOutputStream verbindet den Strom mit einem Eingabestrom.

Eine Pipe-Verbindung zwischen zwei Threads können Sie folgendermaßen herstellen:

PipedInputStream inThread = PipedInputStream();
PipedOutputStream outThread = PipedInputStream(inThread);

Der eine Thread schreibt in outThread und der andere liest von inThread. Ein solches Pärchen ermöglicht eine problemlose Kommunikation zwischen Threads.

13.11 Objektströme

Die Strom-Technik erlaubt es, beliebige Objekte an einen Strom zu schicken oder sie aus ihm zu lesen. Dazu dienen die Schnittstellen ObjectInput und ObjectOutput. Sie definieren Methoden zum Lesen und Schreiben beliebiger Objekte. Die Methoden sind mit den Methode zum Lesen und Schreiben primitiver Typen eng verwandt. Die Schnittstellen ObjectInput und Object-Output erweitern sogar die Schnittstellen DataInput und DataOutput. Die Schnittstelle ObjectInput hat nur eine zusätzliche Eingabe-Methode mit zwei Typen von Ausnahmen:

public abstract Object readObject()  throws ClassNotFoundException 
public abstract Object readObject() throws IOException

Die Schnittstelle ObjectOutput hat ebenfalls eine zusätzliche Ausgabe-Methode:

public abstract void writeObject(Object obj) throws IOException

Der ObjectOutputStream implementiert einen Stromfilter, der es Ihnen ermöglicht, sowohl jedes Objekt in einen Strom zu schreiben als auch jeden primitiven Typen. Wie bei den meisten Stromfiltern können Sie einen ObjectOutputStream durch das Angeben eines OutputStream erstellen:

public OutputStream(OutputStream ausgabeStream)

Mittels der Methode writeObject (drei Ausnahmetypen) können Sie jedes Objekt in einen Strom schreiben:

public final void writeObject(Object ob) throws ClassMismatchException 
public final void writeObject(Object ob) throws MethodMissingException 
public final void writeObject(Object ob) throws IOException

Da der ObjectOutputStream eine Unterklasse des DataOutputStream ist, können Sie alle Methoden der DataOutput-Schnittstelle verwenden, wie beispielsweise writeInt() oder writeUTF().

13.12 Einige spezielle Utility-Ströme

Java stellt eine Reihe von Utility-Filtern zur Verfügung. Diese Filter sind deshalb etwas Besonderes, da sie nicht paarweise existieren, was heißt, dass sie ausschließlich für Eingabe oder Ausgabe arbeiten.

13.12.1 Die LineNumberInputStream-Klasse

Der LineNumberInputStream ermöglicht es, die aktuelle Zeilenzahl eines Eingabestroms zu verfolgen. Dies ist etwa in einem Editor oder Debugger von Bedeutung. Der Filterstrom LineNumberInputStream kann sich sogar eine Zeilennummer merken und sie später in Verbindung mit mark() und reset() verwenden. Der Konstruktor sieht so aus:

public LineNumberInputStream(InputStream eingabeStream)

Die Methode public int getLineNumber() gibt die derzeitige Zeilennummer des Eingabestroms aus. Die Zeilen werden mit 0 beginnend durchnummeriert. Die Zeilenzahl wird immer dann erhöht, wenn eine komplette Zeile gelesen wurde. Die aktuelle Zeilenzahl kann jedoch auch mit der Methode public void setLineNumber(int neueZeilenzahl) festgelegt werden.

13.12.2 Die SequenceInputStream-Klasse

Die SequenceInputStream-Klasse stellt Möglichkeiten zur Verfügung, eine ganze Reihe von Eingabeströmen wie einen einzigen großen Eingabestrom zu behandeln. Etwa bei umfangreichen Leseaktionen über mehrere Ströme hinweg, wenn eine Trennung von verschiedenen Eingabeströmen irrelevant ist. Einen SequenceInputStream, der zwei Ströme verbindet, können Sie erstellen, indem Sie beide Ströme im Konstruktor angeben:

public SequenceInputStream(InputStream stream1, InputStream stream2)

Wenn Sie mehr als zwei Ströme haben wollen, können Sie eine Auflistung der Ströme in Form eines Vektors angeben. Dazu gibt es in Java die Klasse Vector, die natürlich auch noch allgemeiner eingesetzt werden kann.

Beispiel:

Vector v = new Vector(); 
v.addElement(stream1); 
v.addElement(stream2); 
v.addElement(stream3); 
v.addElement(stream4); 
InputStream seq = new SequenceInputStream(v.elements());

Wenn Sie Ströme miteinander kombinieren wollen, können Sie auf diese Art eine Kette von Strömen erstellen.

Beispiel:

InputStream seq = new SequenceInputStream(stream1, new SequenceInputStream(stream2, 
stream3));

13.12.3 Die PushbackInputStream-Klasse

Der PushbackInputStream ist ein spezieller Strom, der es Ihnen erlaubt, ein einzelnes Zeichen eines Eingabestromes zu betrachten und dieses dann wieder in den Strom zurückzuschieben, um die nächste Aktion zu ermitteln. Anwendungen für diese Methode sind beispielsweise Suchroutinen in einer Indexdatenbank. Die Verwandtschaft mit einem gefilterten Eingabestrom ist naheliegend. Der Konstruktor sieht so aus:

public PushbackInputStream(InputStream eingabeStream)

Die Methode public void unread(int ch) throws IOException übergibt ein Zeichen wieder zurück in den Eingabestrom. Dieses Zeichen ist dann das erste, das beim nächsten Mal eingelesen wird, wenn der Eingabestrom erneut gelesen wird.

13.12.4 Die StreamTokenizer-Klasse

Die StreamTokenizer-Klasse hat einen einfachen lexikalischen Abtaster, der einen Zeichenstrom in einen Tokenstrom zerstückelt. Wenn Sie sich einen Zeichenstrom wie einen Satz vorstellen, dann stellen die Token die einzelnen Worte und Satzzeichen dar, aus denen der Satz besteht. Sie erstellen einen StreamTokenizer-Filter, indem Sie ihm den Eingabestrom nennen, den Sie gefiltert haben wollen:

public StringTokenizer(InputStream inStream)

Nachdem Sie den Filter erstellt haben, können Sie die Methode public int nextToken() throws IOException dazu verwenden, diese Token aus dem Strom zu holen. Die nextToken()-Methode gibt entweder ein einzelnes Zeichen oder eine der folgenden Konstanten wieder:

StreamTokenizer.TT_WORD 
StreamTokenizer.TT_NUMBER 
StreamTokenizer.TT_EOL 
StreamTokenizer.TT_EOF

13.13 Die File-Klasse

Als eine der letzten in diesem Kapitel vorgestellten Strom-Klassen wollen wir zu einer der wichtigsten (und historisch ältesten) Verwendungen von Strömen kommen - das Anhängen von Strömen an Dateien im Dateisystem des Rechners. Dies dürfte auch die Ein- und Ausgabefunktionalität von Java sein, die die meisten Leser interessiert. Java stellt die File-Klasse dafür zur Verfügung. Sie kapselt Operationen in Bezug auf das Dateisystem. Darunter fallen die Auflistung des Inhalts von Verzeichnissen, die Erstellung von Verzeichnissen, das Löschen von Dateien oder deren Umbennenung. Abfragen von Datei-Informationen sind andere wichtigere Operationen.

Ein File-Objekt kann sich normalerweise entweder auf eine Datei oder ein Verzeichnis beziehen. Es gibt aber auch Operationen, die sich nur entweder auf eine Datei oder ein Verzeichnis ausführen lassen.

Ein File-Objekt lässt sich auf drei verschiedene Arten erstellen:

public File(String pfadname) 
public File(String pfadname, String dateiname) 
public File(File verzeichnis, String dateiname)

1. Die erste Anweisung erstellt eine File-Instanz, die in pfadname angegeben wurde.
2. Die zweite Anweisung erstellte eine File-Instanz, die sich aus dem in dateiname angegebenen Dateinamen und dem in pfadname angegebenen Pfad zusammensetzt.
3. Die dritte Anweisung erstellt eine File-Instanz, die sich aus dem in dateiname angegebenen Dateinamen und dem in verzeichnis angegebenen Verzeichnis zusammensetzt.

13.13.1 Überprüfung, ob Datei oder Verzeichnis

Viele Operationen der File-Klasse sind unabhängig davon, ob es sich bei dem Objekt um eine Datei oder ein Verzeichnis handelt. Bei anderen Methoden macht es jedoch nur dann Sinn, wenn sie entweder auf eine Datei oder ein Verzeichnis angewandt werden. Sie benötigen also einfache Methoden, mit welchen Sie dies überprüfen können. Dies sind die Methoden public boolean isFile() und public boolean isDirectory().

13.13.2 Lese- und Schreiberlaubnis überprüfen

Genauso wichtig ist die Überprüfung der Dateiattribute, insbesondere der Lese- und Schreiberlaubnis. Mit den Methoden public boolean canRead() und public boolean canWrite() können Sie ermitteln, ob eine Datei oder ein Verzeichnis lesen dürfen und/oder ob Sie dort eine Schreiberlaubnis haben.

13.13.3 Die letzte Änderung überprüfen

Ein anderes wichtiges Dateiattribut können Sie mit der Methode public long lastModified() kontrollieren. Sie gibt eine Zahl aus, die anzeigt, wann die Datei oder das Verzeichnis zuletzt geändert wurde. Da sich der von der lastModified()-Methode ausgegebene Wert in unterschiedlichen Formaten darstellen kann, ist die Methode hauptsächlich zum relativen Vergleich zweier Dateien zu gebrauchen.

13.13.4 Die Existenz eines Objekts überprüfen

Ein File-Objekt muss nach der Erstellung nicht unbedingt als Datei oder Verzeichnis physikalisch existieren. Es kann nur durch einen Dateinamen dargestellt werden. Die Methode public boolean exists() überprüft die physikalische Existenz einer Datei oder einer Verzeichnisses.

13.13.5 Pfadkontrolle

Pfadnamen können entweder relativ oder absolut sein. Ein relativer Pfad bedeutet, dass der Pfad vom aktuellen Verzeichnis aus gesehen wird, während eine absolute Pfadangabe von der Wurzel des Systems ( unter Umständen über lokale Rechner hinaus) ausgeht. Mit der Methode public boolean isAbsolute() lässt sich herausfinden, ob ein gegebenes File-Objekt einen relativen oder absoluten Pfad verwendet.

13.13.6 Namen und Pfad einer Datei oder eines Verzeichnisses ermitteln

Den Namen einer Datei oder eines Verzeichnisses ohne den davor stehenden Pfadnamen liefert die folgende Methode:

public String getName()

Zur Bestimmung des Namens des Verzeichnisses, in dem das File-Objekt enthalten ist, dient die folgende Methode:

public String getParent()

Die Methode public String getPath() gibt den Namen des File-Objekts mit dem davor stehenden Pfadnamen aus, egal ob relativ oder absolut. Den absoluten Pfadnamen eines File-Objekts bekommen Sie mit public String getAbsolutePath().

13.13.7 Eine Datei oder ein Verzeichnis umbenennen

Um eine Datei oder ein Verzeichnis umzubenennen, können Sie in der Methode public boolean renameTo(File neuerName) ein File-Objekt angeben, das den neuen Namen enthält. Die Methode gibt true zurück, wenn die Umbenennung erfolgreich war.

13.13.8 Dateien löschen

Zum Löschen einer Datei können Sie die Methode public boolean delete() verwenden. Die Methode gibt true zurück, wenn die Beseitung erfolgreich war. In bester (?) DOS-Tradition (oder Unix, aber über DOS lässt sich besser lästern :*) ) kann ein Verzeichnis damit jedoch nicht gelöscht werden.

13.13.9 Verzeichnisse erstellen

In Java kann man über die Methode public boolean mkdir() ein Verzeichnis erstellen. Die mkdir()-Methode behandelt das aktuelle File-Objekt wie einen Verzeichnisnamen und versucht, ein Verzeichnis für diesen Namen zu erstellen. Bei erfolgreicher Erstellung eines Verzeichnisses wird true ausgegeben.

Die Methode public boolean mkdirs() ist eine spezielle Variante von der mkdir()-Methode. Im Unterschied zu dieser erstellt sie alle notwendigen Verzeichnisse für den im File-Objekt benannten Pfad. Wenn in der Pfadangabe also Verzeichnisnamen auftauchen, die noch nicht vorhanden sind, werden sie erstellt und das eigentliche Zielverzeichnis existiert dann dort als Unterverzeichnis. Bei erfolgreicher Erstellung eines Verzeichnisses oder einer gesamten Struktur wird true ausgegeben.

13.13.10 Den Inhalt eines Verzeichnisses angeben

Die nur in einem Verzeichnis zu gebrauchende Methode public String[] list() gibt ein Datenfeld der Namen aller Dateien wieder, die in dem Verzeichnis enthalten sind. Sie können für die list()-Methode einen Dateinamen-Filter einrichten, der Ihnen die Auswahl bestimmter Dateinamen ermöglicht:

public String[] list(FilenameFilter filter)

Die Schnittstelle FilenameFilter hat nur eine einzige Methode, nämlich public abstract boolean accept(File verzeichnis, String name), die true ausgibt, wenn der Liste ein Dateiname zugefügt werden soll.

Wir wollen es uns an einem Beispiel ansehen. Das folgende Beispiel implementiert einen Dateinamen-Filter, der nur Dateien zulässt, die die Endung .java haben. Das Ergebnis wird auf dem Bildschirm ausgegeben und funktioniert auf beliebigen Plattformen.

import java.io.*; 
public class ListJava extends Object { 
public static void main(String[] args) { 
// Generiert eine File-Instanz für das 
// aktuelle Verzeichnis 
File currDir = new File("."); 
// Eine gefilterte Liste der .java-Dateien 
// im aktuellen Verzeichnis 
String[] javaDat = currDir.list(new JavaFilter()); 
// Ausgabe des Inhalts des javaDat-Arrays 
for (int i=0; i < javaDat.length; i++) { 
System.out.println(javaDat[i]); 
}   }   } 
class JavaFilter extends Object implements FilenameFilter { 
public boolean accept(File verzeichnis, String name) { 
// Nur dann true, wenn die Datei mit java endet. 
return name.endsWith(".java"); 
}   }

13.14 Dateiströme - FileInputStream und FileOutputStream

Die Dateiströme FileInputStream und FileOutputStream ermöglichen das Lesen und Schreiben von Dateien. Genau genommen werden Ströme an Dateien im Dateisystem angehängt. Dateiströme können aus einer Dateinamen-Zeichenkette, einer File-Instanz oder einem speziellen Datei-Beschreiber erstellt werden:

public FileInputStream(String dateiname) 
public FileInputStream(File datei) 
public FileInputStream(FileDescriptor dateibeschreiber) 
public FileOutputStream(String dateiname) 
public FileOutputStream(File datei) 
public FileOutputStream(FileDescriptor dateibeschreiber)

Um einen Eingabestrom zu erstellen, können Sie wie in folgendem Beispiel vorgehen:

InputStream meinEingabeStream = new FileInputStream("meineDatei");

Um einen Ausgabestrom zu erstellen, können Sie wie in folgendem Beispiel analog vorgehen:

OutputStream meinAusgabeStream = new FileOutputStream("meineDatei");

Die FileDescriptor-Klasse enthält spezielle Informationen über eine geöffnete Datei. Sie erstellen einen FileDescriptor niemals selbst, sondern beziehen ihn durch das Aufrufen der Methode public final FileDescriptor getFD() throws IOException aus einem geöffneten FileInputStream oder FileOutputStream. Im Allgemeinen gibt getFD() den Bezeichner der Datei aus, auf der der Strom basiert. Die FileDescriptor-Klasse enthält eine einzige Methode, nämlich public boolean valid(), die true wiedergibt, wenn ein Datei-Descriptor gültig ist. Die FileDescriptor-Klasse enthält ebenfalls statische Instanzvariablen für die Datei-Descriptoren für Standard-Eingabe, Standard-Ausgabe und Standard-Fehler:

public final static FileDescriptor in 
public final static FileDescriptor out 
public final static FileDescriptor err

13.14.1 Die RandomAccessFile-Klasse

Eine Random-Access-Datei1 ähnelt einem Eingabestrom in der Hinsicht, dass Sie Daten aus ihr lesen können. Gleichzeitig verhält sie sich aber auch wie ein Ausgabestrom, da man Daten in sie hineinschreiben kann (das besagte wahlfreie Verhalten). Der große Unterschied zwischen einer Random Access-Datei und einer sequenziellen Access-Datei (was ein Strom eigentlich ist) liegt darin, dass Sie sofort zu jedem Abschnitt einer Random Access-Datei gehen und dort lesen und schreiben können.

Wenn Sie eine Random Access-Datei erstellen wollen, müssen Sie ihr einen Modus geben. Der Modus ist entweder r (read) oder rw (read-write). Wenn Sie eine Random Access-Datei im read-only-Modus öffnen, können Sie wie üblich keine Daten in sie hineinschreiben. Es gibt keinen write-only-Modus.

Konstruktoren für die RandomAccessFile-Klasse sind:

public RandomAccessFile(String filename, String mode) throws IOException 
public RandomAccessFile(File file, String mode) throws IOException

Die RandomAccessFile-Klasse besitzt alle Methoden, die in den Schnittstellen DataInput und DataOutput verfügbar sind. Zusätzlich beinhaltet sie eine Methode public void seek(long filePosition) throws IOException, die es Ihnen ermöglicht, sofort an jede beliebige Position in der Datei zu springen.

Sie können auch die aktuelle Dateiposition bestimmen, was recht nützlich ist, wenn Sie vorhaben, an diese Dateiposition zurückzuspringen, indem Sie die Methode public long getFilePointer() throws IOException verwenden. Der Dateipositionswert, der in den Methoden seek() und getFilePointer() benutzt wird, ist die Anzahl der Bytes von Anfang bis zum Ende der Datei.

13.15 Praktische Java-Beispiele mit Datenströmen

Dieses Kapitel war bisher ziemlich umfangreich und bestand im Wesentlichen aus viel Theorie (ich habe am Anfang des Kapitels gewarnt). Nun soll der Theorie wieder mehr Praxis folgen, wobei gerade das Kapitel mit den erweiterten Java-Techniken noch diverse weitere Beispielen beinhaltet. Wir wollen zuerst ein Java-Programm erstellen, das eine Datei öffnet und die Daten nach dem Einlesen verarbeitet und dann wieder in eine andere Datei schreibt. Die dazu notwendigen Techniken haben wir uns mittlerweile erarbeitet. Zudem basiert das Programm auf unserem ersten Beispiel in diesem Kapitel. Es werden nur ein paar interessante Details verändert. Aber klären wir zuerst, was das Programm leisten soll:

  • Eine beliebige Datei lesen
  • Eine Datei erstellen, deren Name Sie frei wählen können
  • Die Daten auf nützliche Art und Weise verarbeiten. Wir werden einen Verschlüsselungs- und Entschlüsselungsalgorithmus implementieren.

Wir werden also unser Kopierprogramm vom Anfang in ein Kodierungsprogramm umschreiben, das beliebige Binärdateien (Texte, Grafiken, Programme usw.) kodieren und wieder entschlüsseln kann. Dazu verwenden wir dieses Mal auch andere Lese- und Schreibmethoden. Das Programm muss in unserem Fall nicht einmal Multithreading-fähig sein, da wir das Einlesen eines Bytes, das Kodieren/Dekodieren und das Schreiben chronologisch abarbeiten. Wir werden auch mehrere Varianten des Programms erstellen, die verschiedene Techniken nutzen.

Ansonsten ist das erste Programm recht einfach gehalten. Die Übergabeparameter müssen weitgehend korrekt eingegeben werden. Hilfe ist nur eingeschränkt vorhanden. Das Dateiende wird zwar in einer catch-Klausel aufgefangen, I/O-Ausnahmen werden jedoch einfach weitergereicht und die Ausgabe nach getaner Arbeit beschränkt sich auf die Meldung »Alles klar! Kodierung beendet.« bzw. »Alles klar! Dekodierung beendet.«. Außerdem findet keine Kontrolle statt, ob die zu erstellende Datei evtl. schon vorhanden ist und überschrieben wird. Nicht zuletzt ist eine grafische Oberfläche sicher ein lohnender Ansatz. Aber warten Sie damit erst einmal. Wir erweitern das Beispiel noch um Elemente des AWTs.

Sie haben nun viele Ansatzpunkte, wie das Programm verbessert werden kann. Dennoch - es ist vom Ansatz her ein vollständiges Kodierungsprogramm. Der verwendete Algorithmus ist die Cäsar-Chiffre, ein einfacher Verschiebungsalgorithmus. Diese Verschlüsselungsmethode lautet wie folgt: Für jedes im Quelltext vorkommende Zeichen setze im Chiffretext ein um einen festen Parameter versetztes Zeichen. Wenn beispielsweise im Quelltext der erste Buchstabe des Alphabets - ein »A« - auftaucht und der Verschlüsselungsparameter »3« ist (angeblich der von Cäsar benutzte Parameter), wird im Chiffretext der vierte Buchstabe des Alphabetes - ein »D« - genommen usw. Das Dekodieren funktioniert genau umgekehrt. Zugegebenermaßen ist die Technik sehr einfach, bei binären Dateien ist die einfache Cäsar-Chiffre trotzdem wirkungsvoll. Durch die bereits in der Quelldatei vorkommenden Steuerzeichen ist ein Erraten der verwendeten Konstanten extrem erschwert. Überdies kann man alleine durch eine solche Verschiebung von Zeichen automatische Spionage-Tools in einem Netzwerk (z.B. dem Internet) austricksen, die Texte nach bestimmten Kennworten (z.B. Geheimzahl oder Kreditkartennummer) scannen.

Mehr zu der Arbeitsweise und Theorie von Verschlüsselungsverfahren finden Sie im Anhang. Gehen wir zum eigentlichen Java-Source über.

/* Ein Kodierungsprogramm für beliebige binäre Dateien */
import java.io.*;
public class Kodier {
/* Die Kodierungsmethode. Als Übergabeargumente werden die Quelldatei und die zu erstellende 
Zieldatei verwendet. */
public static void kodiere(String quelldatei, String zieldatei) throws IOException {
// Erstellen eines Eingabe- und eines 
// Ausgabestroms
DataInput quelle = new DataInputStream(new FileInputStream(quelldatei));
DataOutput ziel = new DataOutputStream(new FileOutputStream(zieldatei));
try {
 while(true)  {
// Einlesen eines Bytes aus der Quelle
   byte b = (byte) quelle.readByte();  
/* Hier findet die Verschiebung statt. Wir kümmern uns selbst um den Wertebereich des 
Datentyps byte, indem wir den Modulo-Operator verwenden.*/
   b = (byte)((b + 3)%256); 
// Schreiben eines Bytes in die Zieldatei
   ziel.writeByte(b);  
  }  }
// Dateiende der Quelldatei erreicht.
catch (EOFException e)  {
System.out.println(
"Alles klar! Kodierung beendet.");
}  }
/* Die Dekodierungsmethode */
public static void dekodiere(String quelldatei, String zieldatei) throws IOException {
// Erstellen eines Eingabe- und eines 
// Ausgabestroms
DataInput quelle = new DataInputStream(new FileInputStream(quelldatei));
DataOutput ziel = new DataOutputStream(new FileOutputStream(zieldatei));
try {
 while(true)  {
// Einlesen eines Bytes aus der Quelle
   byte b = (byte) quelle.readByte();
/* Hier findet die Verschiebung zurück statt. Wir kümmern uns selbst um den Wertebereich des 
Datentyps byte, indem wir den Modulo-Operator verwenden. */
   b = (byte)((b - 3)%256);
   ziel.writeByte(b);
  }  }
// Dateiende der Quelldatei erreicht.
catch (EOFException e) {
System.out.println(
"Alles klar! Dekodierung beendet.");
}  }
public static void main (String args[]) throws IOException {
  if ((args[2].equals("k")) ||(args[2].equals("K")))  {
 kodiere(args[0],args[1]);  
}
 else if ((args[2].equals("d")) ||(args[2].equals("D"))) dekodiere(args[0],args[1]);     
  else  {
  System.out.println(
  "Sie muessen folgende Syntax eingeben:");
  System.out.println("java Kodier [Name der Quelldatei] [Name der Zieldatei] [Kodier/
Dekodier]");
   System.out.println("Dabei bedeutet [Kodier/Dekodier] Folgendes:");
   System.out.println("Wenn Sie ein k oder K eingeben, kodieren Sie.");
   System.out.println("Wenn Sie ein d oder D eingeben, dekodieren Sie.");
 }  }  }

Probieren Sie das Programm ruhig mit beliebigen Dateien aus. Es sollte problemlos alle Dateien verarbeiten können. Beachten Sie, dass eine Hilfe bei zu wenig Übergabeparametern nicht unterstützt wird und eine nicht abgefangene Ausnahme als Rückmeldung erscheint.

Wir wollen nun das Beispiel etwas erweitern. Dabei nehmen wir uns mehrerer Punkte an:

  • Ein anderer Datenstrom. Diesmal arbeiten wir mit RandomAccessFiles zum Erstellen eines Eingabe- und eines Ausgabestroms.
  • In der Kodier- und der Dekodier-Methode sichert ein doppelter try-Block (try-catch und try-finally) die Aktionen ab. Im finally-Teil werden die Quell- und Zieldatei mit der close()-Methode geschlossen.
  • Wir fragen die Größe der zu kodierenden oder dekodierenden Datei ab und geben Sie in Byte aus. Wegen der UTF-Kodierung von Java muss der mit length() ermittelte Wert durch 2 geteilt werden. Wir werden diese Angabe hier noch nicht weiter verwenden. Denkbar (und meist sinnvoll) sind Statusanzeigen, die angeben, wie weit die Aktion gediehen ist.
  • Der Kodierungsalgorithmus wird bei Textdateien sicherer. Die hier verwendete Verschiebung ist größer als in unserem ersten Beispiel. Damit wird bei Textdateien eine bessere Verschleierung erreicht, da die Zeichen in der Zieldatei keine Buchstaben mehr sind. Nicht lesbare Zeichen lassen sich ohne Hilfsmittel schlechter entschlüsseln (obwohl es immer noch ein sehr einfaches Verfahren ist).
  • Eine ganz entscheidende Erweiterung finden Sie in der main()-Methode. Diese gibt die von der Kodier- und Dekodier-Methode weitergereichten Ausnahmen nicht einfach weiter, sondern sie werden in dieser Version der main()-Methode abgefangen. Dazu dient der try-catch-Block der main()-Methode. Er fängt sowohl I/O-Ausnahmen als auch ArrayIndexOutOfBoundsException ab.

Wenden wir uns nun dem konkreten Source zu.

/* Ein Kodierungsprogramm für beliebige binäre Dateien */
import java.io.*;
public class Kodier2 {
/* Die Kodierungsmethode. Als Übergabeargumente werden die Quelldatei und die zu erstellende 
Zieldatei verwendet. */
public static void kodiere(String quelldatei, String zieldatei) throws IOException {
/* Erstellen eines Eingabe- und eines Ausgabestroms. Dieses Mal arbeiten wir mit 
RandomAccessFiles */
// Quelle nur zum Lesen öffnen
RandomAccessFile quelle= new RandomAccessFile(quelldatei,"r");  
// Zu erstellende Datei mit Lese-/Schreibzugriff 
// öffnen
RandomAccessFile ziel = new RandomAccessFile(zieldatei,"rw");
try {
try {
// Abfrage der Dateigröße
 long laenge = quelle.length();
System.out.println("Die Groesse der zu kodierenden Datei ist " + laenge + " Byte.");
 while(true)  {
// Einlesen eines Bytes aus der Quelle
   byte b = (byte) quelle.readByte();  
/* Hier findet die Verschiebung statt. Wir kümmern uns selbst um den Wertebereich des 
Datentyps byte, indem wir den Modulo-Operator verwenden. Die hier verwendete Verschiebung ist 
größer als in unserem ersten Beispiel. Damit wird bei Textdateien eine bessere Verschleierung 
erreicht, da die Zeichen keine Buchstaben mehr sind.*/
   b = (byte)((b + 42)%256); 
// Schreiben eines Bytes in die Zieldatei
   ziel.writeByte(b);    
}  }
catch (EOFException e) {
// Dateiende der Quelldatei erreicht.
// Meldung aus dem catch-Teil
System.out.println("Alles klar! EOFException zeigt Dateiende an. Kodierung beendet.");
}  }
finally {
quelle.close();  //Quelldatei schließen
ziel.close();  // Erstellte Datei schließen
// Meldung aus dem finally-Teil
System.out.println("Der finally-Abschnitt ist auch beendet.");
}  }
/* Die Dekodierungsmethode */
public static void dekodiere(String quelldatei, String zieldatei) throws IOException {
/* Erstellen eines Eingabe- und eines Ausgabestroms. Hier sind auch wieder RandomAccessFiles 
verwendet */
// Quelle nur zum Lesen öffnen
RandomAccessFile quelle= new RandomAccessFile(quelldatei,"r");  
// Zu erstellende Datei mit Lese-/Schreibzugriff 
// öffnen
RandomAccessFile ziel = new RandomAccessFile(zieldatei,"rw");
try {
try {
// Abfrage der Dateigröße 
long laenge = quelle.length();
System.out.println("Die Groesse der zu dekodierenden Datei ist " + laenge + " Byte.");
 while(true)  { 
// Einlesen eines Bytes aus der Quelle
   byte b = (byte) quelle.readByte(); 
/* Hier findet die Verschiebung statt. Wir kümmern uns selbst um den Wertebereich des 
Datentyps byte, indem wir den Modulo-Operator verwenden. Die hier verwendete Verschiebung ist 
größer als in unserem ersten Beispiel. Damit wird bei Textdateien eine  bessere 
Verschleierung erreicht, da die Zeichen keine Buchstaben mehr sind.*/
   b = (byte)((b - 42)%256);  
// Schreiben eines Bytes in die Zieldatei
   ziel.writeByte(b); 
  }  }
catch (EOFException e) {
 // Dateiende der Quelldatei erreicht.
// Meldung aus dem catch-Teil
System.out.println("Alles klar! EOFException zeigt Dateiende an. Deodierung beendet.");
}  }
finally {
quelle.close();  //Quelldatei schließen
ziel.close();  // Erstellte Datei schließen
// Meldung aus dem finally-Teil
System.out.println("Der finally-Abschnitt ist auch beendet.");
}  }
public static void main (String args[])  {
/* Die Ausnahmen werden in dieser Version der main()-Methode abgefangen. Dazu dient der try-
catch-Block. Er fängt sowohl I/O-Ausnahmen als auch  ArrayIndexOutOfBoundsException ab. 
Letztere tritt auf, wenn zu wenig Übergabeparameter eingegeben werden*/  try {
  if ((args[2].equals("k")) ||(args[2].equals("K")))   {
kodiere(args[0],args[1]);  
}
 else if ((args[2].equals("d")) ||(args[2].equals("D"))) dekodiere(args[0],args[1]);     
  else  {
   System.out.println("Sie muessen folgende Syntax eingeben:");
   System.out.println("java Kodier [Name der Quelldatei] [Name der Zieldatei] [Kodier/
Dekodier]");
   System.out.println("Dabei bedeutet [Kodier/Dekodier] Folgendes:");
   System.out.println("Wenn Sie ein k oder K eingeben, kodieren Sie.");
   System.out.println("Wenn Sie ein d oder D eingeben, dekodieren Sie.");
  }  }
catch (IOException e) {
System.out.println("Es gibt eine IOException: " + e.getMessage());
}
catch (ArrayIndexOutOfBoundsException e) {
System.out.println("ArrayIndexOutOfBoundsException!" );
System.out.println("Sie muessen folgende Syntax eingeben:");
System.out.println("java Kodier [Name der Quelldatei] [Name der Zieldatei] [Kodier/
Dekodier]");
System.out.println("Dabei bedeutet [Kodier/Dekodier] Folgendes:");
System.out.println("Wenn Sie ein k oder K eingeben, kodieren Sie.");
System.out.println("Wenn Sie ein d oder D eingeben, dekodieren Sie.");
}  }  }

Als ein Highlight dieses Kapitels wollen wir uns noch einmal dem Kodierungsprogramm widmen und ihm eine grafische Oberfläche zur Verfügung stellen. Die Kodierungsfunktionalität war schon recht weit gediehen und soll nur sehr geringfügig überarbeitet werden.

Was jedoch noch sicher ausbaufähig ist, ist die Benutzerkommunikation. Sicher - Befehlszeilen mit Übergabeparameter haben zwar ihren Reiz, sind für Experten effektiver als umständliche und langwierige Fenster, Menüs und Dialogstrukturen, aber es sitzen nicht nur Experten an den Rechnern. Kurz gesagt - über den Nutzen einer grafischen Oberfläche kann man zwar streiten, aber nur, wenn man auf der Expertenseite steht :-). Gehen wir das Projekt an.

Für unser Programm genügt es, wenn wir die Möglichkeiten des AWT-1.0-Modells und das Eventmodell 1.0 nutzen. Insbesondere verzichten wir auch auf JFC und Swing. Im Prinzip ist das Programm - so wie wir es erstellt haben - auch als Applet einsetzbar (auch in Browsern, die nur Java 1.0.2 verstehen). Dazu muss nur eine boolesche Variable umgesetzt werden. Beachten Sie aber, dass in der Regel die Sicherheitseinstellungen von Viewern die Dateizugriffe unterbinden.

Was muss nun ein solches Kodierungsprogramm mit einer Benutzeroberfläche - neben der eigentlichen Kodierung - leisten?

  • Eine Benutzeroberfläche soll klar machen, worum es geht. Wir werden über verschiedene Layoutmanager in verschachtelten Panels eine Oberfläche gestalten (mit Schaltflächen und Menüleiste).
  • Es muss eine Auswahlmöglichkeit für die zu kodierende Datei und die zu schreibende Datei geben. Dazu werden wir zwei File-Dialoge zur Verfügung stellen. Das genaue Datei-Handling (etwa nur existierende Datei zum Kodieren auswählen und beim Überschreiben einer bestehenden Datei warnen) übernimmt der jeweilige File-Dialog.
  • Es muss sichergestellt sein, dass der eigentliche Kodierungsvorgang (bzw. Dekodierungsvorgang) nur dann gestartet werden kann, wenn sowohl Quell- als auch Zieldatei vorher festgelegt wurden. Wir regeln das über Deaktivieren und Aktivieren der jeweiligen Schaltflächen je nach Kontext.
  • Wichtige Fehlermeldungen müssen in Dialogfenstern ausgegeben werden, ebenso Hinweise. Das bedeutet, die Meldungen beim Auftreten von Ausnahmen werden nicht mehr ausschließlich auf Systemebene angezeigt, sondern auch in Fehlerfenstern.
  • Ein Hilfe-Menü steht zur Verfügung. Dabei verwenden wir Dialogfenster und Textfelder.

Beachten Sie den Aufbau des Hilfetextes und des Infotextes. Die beiden Texte gehen über mehrere Textzeilen. Ohne Eingabe eines expliziten Zeilenvorschubs wird der ganze Text jeweils in einer Zeile dargestellt. Man müßte also erheblich seitwärts scrollen. Wie schon bei der Behandlung der Unicode-Zeichenliterale angedeutet, werden diese schon zu einem sehr frühen Zeitpunkt vom Java-Compiler javac interpretiert. Wenn man daher die Escape-Unicode-Literale dazu verwendet, einen expliziten Zeilenvorschub zu erzeugen (\u000a), wird das Zeilenende-Zeichen vor dem schließenden einfachen Anführungszeichen erscheinen. Das Resultat ist dann ein Kompilierfehler. Wir haben also statt dessen das Escape-Literal \n (im Hilfetext) und als Alternative die oktale Darstellung \012 (im Infotext) verwendet.

Abbildung 13.3:  Die Oberfläche des Kodierungsprogramms. Die beiden äußeren Buttons sind deaktiviert.

/* Ein Kodierungsprogramm für beliebige binäre Dateien mit grafischer Oberfläche. Die 
Aktionen werden allesamt in einer grafischen Bedienoberfläche durchgeführt. Die Ausgaben über 
System.out.println dienen nur dazu, den Ablauf des Programms zu verdeutlichen und können 
auskommentiert oder gelöscht werden. Noch sinnvoller ist es, einige dieser Meldungen in Form 
von Hinweisfenstern auszugeben. Für einige Fälle ist dies bereits realisiert. Sie können 
diese entsprechend erweitern. */
import java.io.*;
import java.applet.*;
import java.awt.*;
public class Rjskryp extends Applet {
// STANDALONE APPLICATION
/* Die boolesche Variable StandAlone_ja wird auf true gesetzt, wenn das Programm als 
Standalone- Applikation gestartet wird. Dies ist in unserem Fall sowieso der Fall. Im Prinzip 
kann das Programm auch als Applet fungieren, wobei die Sicherheitsbeschränkungen des Viewers 
beachtet werden müssen. */
//----------------------------------------------
 private boolean StandAlone_ja= false;
/* Die Kodierungsmethode. Als Übergabeargumente werden die Quelldatei und die zu erstellende 
Zieldatei verwendet. */
public static void kodiere(String quelldat, String zieldat) throws IOException {
// Erstellen eines Eingabe- und eines 
// Ausgabestroms. Dieses Mal arbeiten wir mit 
// RandomAccessFiles.
// Quelle nur zum Lesen öffnen
RandomAccessFile quelle= new RandomAccessFile(quelldat,"r");
// Erstellende Datei mit Lese-/Schreibzugriff 
// öffnen
RandomAccessFile ziel = new RandomAccessFile(zieldat,"rw");
try {
try {
 while(true) {
// Einlesen eines Bytes aus der Quelle
 byte b = (byte) quelle.readByte();
/* Hier findet die Verschiebung statt. Wir kümmern uns selbst um den Wertebereich des 
Datentyps byte, indem wir den Modulo-Operator verwenden. Verschiebung 111*/
 b = (byte)((b + 111)%256);
 ziel.writeByte(b); // Schreiben eines Bytes in die Zieldatei
 }  }
catch (EOFException e) {
// Dateiende der Quelldatei erreicht.
// Meldung aus dem catch-Teil auf Systemebene
System.out.println("Alles klar! EOFException zeigt Dateiende an. Kodierung beendet.");
}  }
finally {
quelle.close(); //Quelldatei schließen
ziel.close(); // Erstellte Datei schließen
// Meldung aus dem finally-Teil
System.out.println("Der finally-Abschnitt ist auch beendet.");
}  }
/* Die Dekodierungsmethode */
public static void dekodiere(String quelldat, String zieldat) throws IOException {
/* Erstellen eines Eingabe- und eines Ausgabestroms. Hier sind auch wieder RandomAccessFiles 
verwendet */
// Quelle nur zum Lesen öffnen
RandomAccessFile quelle= new RandomAccessFile(quelldat,"r");
// Zu erstellende Datei mit Lese-/Schreibzugriff 
// öffnen
RandomAccessFile ziel = new RandomAccessFile(zieldat,"rw");
try {
try {
 while(true) {
// Einlesen eines Bytes aus der Quelle
 byte b = (byte) quelle.readByte();
/* Hier findet die Verschiebung statt. Wir kümmern uns selbst um den Wertebereich des 
Datentyps byte, indem wir den Modulo-Operator verwenden. Verschiebung 111 */
 b = (byte)((b - 111)%256);
 ziel.writeByte(b); // Schreiben eines Bytes in die Zieldatei
 }  }
catch (EOFException e) {
// Dateiende der Quelldatei erreicht.
// Meldung aus dem catch-Teil
System.out.println("Alles klar! EOFException zeigt Dateiende an. Deodierung beendet.");
}  }
finally {
quelle.close(); //Quelldatei schließen
ziel.close(); // Erstellte Datei schließen
// Meldung aus dem finally-Teil
System.out.println("Der finally-Abschnitt ist auch beendet.");
}  }
public static void main (String args[]) {
alleineFensterFrame fenster = new alleineFensterFrame("Rjskryp");
fenster.resize(fenster.insets().left + fenster.insets().right + 640,
fenster.insets().top + fenster.insets().bottom + 400);
Rjskryp applet_Rjskryp = new Rjskryp();
fenster.add("Center", applet_Rjskryp);
applet_Rjskryp.StandAlone_ja= true; // Standalone-Programm
fenster.show();
}  }
class alleineFensterFrame extends Frame {
// Festlegung diverser Panels
Panel hauptPanel = new Panel();
Panel ueberschriftPanel = new Panel();
Panel beschreibPanel = new Panel();
Panel buttonPanel = new Panel();
Panel westPanel = new Panel();
Panel ostPanel = new Panel();
// Beschriftungen
Label meinTitel = new Label(
"RJSKRYP - Javaversion");
Label beschreib = new Label(
"Kodiert und dekodiert beliebige binäre Dateien");
Label beschreiblinks = new Label("Kodieren");
Label beschreibrechts = new Label("Dekodieren");
// Einige unterschiedliche Fonts
Font meinTitelFont = new Font("TimesRoman", Font.BOLD, 36);
Font meinBeschreibFont = new Font("TimesRoman", Font.ITALIC, 20);
Font signalFont = new Font("TimesRoman", Font.PLAIN, 24);
// Das Menü
MenuBar meineMenueLeiste;
Menu meinMenue;
// Die Dialog-, Fehler- und Hinweisfenster
TextDialog meinHilfeFenster;
TextDialog meinInfoFenster;
TextDialog meinFehlerFenster;
TextDialog meinKodierFenster;
TextDialog meinDekodierFenster;
// Die File-Dialoge
Dateizugriffe kodiereDateiFenster;
Dateizugriffe dekodiereDateiFenster;
// Variablen, in denen die Pfadangaben der
// zu bearbeitenden Dateien gespeichert werden
String zuLesendeDatei = "";
String zuSchreibendeDatei = "";
String PfadzuLesendeDatei;
String PfadzuSchreibendeDatei;
// Schaltflächen
Button Kodier = new Button("Kodieren");
Button Dekodier = new Button("Dekodieren");
public alleineFensterFrame(String str) {
 super (str);
 // Setze BorderLayout für das Frame 
// als Hauptpanel
 this.setLayout(new BorderLayout());
 // Hintergrundfarbe gesamt
 setBackground(Color.blue);
 // Füge die vier Panels hinzu
 this.add("North", hauptPanel);
 this.add("South", buttonPanel);
 this.add("West", westPanel);
 this.add("East", ostPanel);
//BorderLayout im Hauptpanel
 hauptPanel.setLayout(new BorderLayout());
// ueberschriftPanel und beschreibPanel sind
// Subpanels von hauptpanel
 hauptPanel.add("North", ueberschriftPanel);
 hauptPanel.add("South", beschreibPanel);
 /* Überschrift */
// Überschriftfont
 meinTitel.setFont(meinTitelFont);
// spezielle Schriftfarbe Überschriftpanel
 ueberschriftPanel.setForeground(Color.white);
// Überschrift in Überschriftpanel
 ueberschriftPanel.add(meinTitel);
 /* Die Beschreibung im Süden des HauptPanels */
// Beschreibungsfont
 beschreib.setFont(meinBeschreibFont);
// spezielle Schriftfarbe Beschreibungpanel
 beschreibPanel.setForeground(Color.white);
// Text in Beschreibungspanel
 beschreibPanel.add(beschreib);
 beschreiblinks.setFont(signalFont);
// spezielle Schriftfarbe
 westPanel.setForeground(Color.red);
 westPanel.add(beschreiblinks);
 beschreibrechts.setFont(signalFont);
// spezielle Schriftfarbe
 ostPanel.setForeground(Color.red);
 ostPanel.add(beschreibrechts);
 /* Die Schaltflächen */
//FlowLayout im Südpanel
 buttonPanel.setLayout(new FlowLayout());
// Schaltflächen hinzufügen
 buttonPanel.add( Kodier);
 buttonPanel.add( new Button(
  "Zu lesende Datei auswählen") );
 buttonPanel.add( new Button("Ende") );
 buttonPanel.add( new Button(
  "Zu schreibende Datei auswählen") );
 buttonPanel.add( Dekodier );
 Kodier.disable();
 Dekodier.disable();
 // Menubar erstellen
 meineMenueLeiste = new MenuBar();
 // Menu zusammenbasteln
 meinMenue = new Menu("Datei");
 meinMenue.add(new MenuItem("Programm Beenden"));
 // 1. Menu zur Menubar hinzufügen
 meineMenueLeiste.add(meinMenue);
 // Menubar an den Rahmen binden
 setMenuBar(meineMenueLeiste);
 // Das Hilfemenü
 Menu meineHilfe = new Menu("Hilfe");
 meineMenueLeiste.setHelpMenu(meineHilfe);
 // Einträge im Hilfemenü
 meineHilfe.add(new MenuItem("Info"));
 meineHilfe.add(new MenuItem("Hilfe"));
// Erstelle Hilfe-Dialog
 meinHilfeFenster = new TextDialog(this, 
 "Hilfe zu RJSKRYP - Javaversion", true, 0,"");
 meinHilfeFenster.resize(500,230);
// Erstelle Info-Dialog
 meinInfoFenster = new TextDialog(this, 
 "Info zu RJSKRYP - Javaversion", true, 1,"");
 meinInfoFenster.resize(450,220);
// Erstelle Kodierende-Hinweis
 meinKodierFenster = new TextDialog(this, "Hinweis", true, 3,"");
 meinKodierFenster.resize(180,150);
// Erstelle Dekodierende-Hinweis
 meinDekodierFenster = new TextDialog(this, "Hinweis", true, 4,"");
 meinDekodierFenster.resize(180,150);
// Erstelle Kodier-Dialog
 kodiereDateiFenster = new Dateizugriffe(this, "Datei kodieren",FileDialog.LOAD);
 kodiereDateiFenster.resize(350,150);
// Erstelle Dekodier-Dialog
 dekodiereDateiFenster = new Dateizugriffe(this, "Datei dekodieren",FileDialog.SAVE);
 dekodiereDateiFenster.resize(350,150);
}
public boolean action(Event ev, Object arg) {
 String label = (String)arg;
 if ( ev.target instanceof Button ) {
 // Wenn Schaltfläche "Ende"
 if (label.equals("Ende")) {
 this.dispose();
 System.exit(0);
 }
 else if (label.equals(
  "Zu lesende Datei auswählen")) {
 kodiereDateiFenster.show();
 PfadzuLesendeDatei = kodiereDateiFenster.getDirectory() + kodiereDateiFenster.getFile();
 zuLesendeDatei = kodiereDateiFenster.getFile();
 System.out.println(zuLesendeDatei);
/* Im Folgeschritt wird sichergestellt, dass die Schaltflächen zum Kodieren bzw. Dekodieren 
nur dann aktivierbar sind, wenn sowohl eine Quell-, als auch eine Zieldatei ausgewählt sind. 
Falls dies gewährleistet ist, kann die Kodierungs- bzw. Dekodierungsmethode aufgerufen 
werden. Beachten Sie, dass dabei nicht ueberprüft wird, ob die Quelldatei so gewählt wurde, 
dass auch nur eine bereits kodierte Datei dekodiert wird. Dies macht das Programm zwar 
gefährlicher, aber andererseits auch sicherer gegen unberechtigte Anwendung. */
 if ((zuLesendeDatei.equals("")) ||(zuSchreibendeDatei.equals(""))) {
 Kodier.disable();
 Dekodier.disable();
 }
 else {
 System.out.println(zuSchreibendeDatei);
 System.out.println(zuLesendeDatei);
 Kodier.enable();
 Dekodier.enable();
 }
 }
 else if (label.equals(
  "Zu schreibende Datei auswählen")) {
 dekodiereDateiFenster.show();
 PfadzuSchreibendeDatei = dekodiereDateiFenster.getDirectory() + 
dekodiereDateiFenster.getFile();
 zuSchreibendeDatei = dekodiereDateiFenster.getFile();
 System.out.println(zuSchreibendeDatei);
 if ((zuLesendeDatei.equals("")) ||(zuSchreibendeDatei.equals(""))) {
 Kodier.disable();
 Dekodier.disable();
 }
 else {
 System.out.println(zuSchreibendeDatei);
 System.out.println(zuLesendeDatei);
 Kodier.enable();
 Dekodier.enable();
 }
 }
 else if (label.equals("Kodieren")) {
 try {
 Rjskryp.kodiere(zuLesendeDatei, zuSchreibendeDatei);
 meinKodierFenster.show(); // Ende-Hinweis
 }
 catch (IOException e) {
 // Ausgabe auf Systemebene
 System.out.println("Es gibt eine IOException: " + e.getMessage());
 // Erstelle Fehler-Dialog
 meinFehlerFenster = new TextDialog(this, "Fehlermeldung", true, 2,
 "Probleme beim Schreiben oder Schreiben: " + 
 e.getMessage());
 meinFehlerFenster.resize(450,150);
 meinFehlerFenster.show();
 }
 catch (ArrayIndexOutOfBoundsException e) {
 // Ausgabe auf Systemebene
 System.out.println(
 "Es gibt eine ArrayIndexOutOfBoundsException: " 
 + e.getMessage());
 // Erstelle Fehler-Dialog
 meinFehlerFenster = new TextDialog(this, "Fehlermeldung", true, 2,
 "Es gibt eine ArrayIndexOutOfBoundsException: " 
 + e.getMessage());
 meinFehlerFenster.resize(450,150);
 meinFehlerFenster.show();
 }
 }
 else if (label.equals("Dekodieren")) {
 try {
 Rjskryp.dekodiere(zuLesendeDatei, zuSchreibendeDatei);
 meinDekodierFenster.show(); // Endehinweis
 }
 catch (IOException e) {
 // Ausgabe auf Systemebene
 System.out.println("Es gibt eine IOException: " + e.getMessage());
 // Erstelle Fehler-Dialog
 meinFehlerFenster = new TextDialog(this, "Fehlermeldung", true, 2,
 "Probleme beim Schreiben oder Schreiben: " + 
 e.getMessage());
 meinFehlerFenster.resize(450,150);
 meinFehlerFenster.show();
 }
 catch (ArrayIndexOutOfBoundsException e) {
 // Ausgabe auf Systemebene
 System.out.println(
 "Es gibt eine ArrayIndexOutOfBoundsException: " 
 + e.getMessage());
 // Erstelle Fehler-Dialog
 meinFehlerFenster = new TextDialog(this, "Fehlermeldung", true, 2,
 "Es gibt eine ArrayIndexOutOfBoundsException: " 
 + e.getMessage());
 meinFehlerFenster.resize(450,150);
 meinFehlerFenster.show();
 }
 }
 }
// Reaktionen auf reguläre Menüeinträge
 else if (ev.target instanceof MenuItem) {
 if (label.equals("Programm beenden")) {
 this.dispose();
 System.exit(0);
 }
 else if (label.equals("Info")) {
 meinInfoFenster.show();
 }
 else if (label.equals("Hilfe")) {
 meinHilfeFenster.show();
 }
 }
 return true;
 }
 public boolean handleEvent(Event evt) {
 switch (evt.id) {
 case Event.WINDOW_DESTROY:
 dispose();
 System.exit(0);
 return true;
 default:
 return super.handleEvent(evt);
 }
 }  }
// Dialog-Klasse
class TextDialog extends Dialog {
String hilfeText = "RJSKRYP für Java kodiert und dekodiert beliebige binäre Dateien."
+ "\nBedienung: \nSie wählen zunächst eine Datei als Quelle und eine als Zieldatei aus."
+ "\nAnschließend werden die Schaltflächen zum Kodieren und Dekodieren aktivierbar."
+ "\nDanach entscheiden Sie, ob Sie eine Datei ver- oder entschlüsseln wollen 
\n(entsprechende Push-Button).";
String infoText = "RJSKRYP für Java wurde entwickelt von RJS-EDV-KnowHow."
+ "\012Version 1.1"
+ "\012\012Besuchen Sie uns im Internet: "
+ "\012http://www.rjs.de";
TextArea meinHilfeText = new TextArea(hilfeText, 5, 42);
TextArea meinInfoText = new TextArea(infoText, 3, 40);
TextArea meinKodierText = new TextArea(
  "Kodierung beendet", 3, 40);
TextArea meinDekodierText = new TextArea("Dekodierung beendet", 3, 40);
TextDialog(Frame aufrufendesFenster, String title, boolean modal, int welcherText, String 
text) {
 super(aufrufendesFenster, title, modal);
 setLayout(new BorderLayout(10,10));
 setBackground(Color.green);
 add("South", new Button("Alles klar"));
 if (welcherText==0) {
 meinHilfeText.setEditable(false);
 add("Center",meinHilfeText);
 }
 else if (welcherText==1) {
 meinInfoText.setEditable(false);
 add("Center",meinInfoText);
 }
 else if (welcherText==2) {
 TextArea meinFehlerText = new TextArea(text, 3, 40);
 meinFehlerText.setEditable(false);
 add("North",meinFehlerText);
 }
 else if (welcherText==3) {
 meinKodierText.setEditable(false);
 add("North",meinKodierText);
 }
 else if (welcherText==4) {
 meinDekodierText.setEditable(false);
 add("North",meinDekodierText);
 }
 }
 public Insets insets() {
 return new Insets(40,10,10,10);
}
// action()-Methode des Dialogfensters
 public boolean action(Event evt, Object arg) {
 String label = (String)arg;
 if (evt.target instanceof Button) {
 if (label == "Alles klar") {
 hide(); // Dialog schließen
 }
 }
 else return false;
 return true;
 } // Ende action()-Methode des Dialogs
 public boolean handleEvent(Event evt) {
 switch (evt.id) {
 case Event.WINDOW_DESTROY:
 dispose();
 return true;
 default:
 return super.handleEvent(evt);
 }
 }  }
// FileDialogklasse
class Dateizugriffe extends FileDialog {
 Dateizugriffe(Frame aufrufendesFenster, String title, int art) {
 super(aufrufendesFenster, title, art);
}  }

Wenn Sie das Programm starten, werden die beiden Schaltflächen zur Kodierung und Dekodierung deaktiviert sein. Sie müssen, bevor sie aktiviert werden, sowohl eine Datei als Quelle als auch eine als Ziel auswählen. Dazu werden Standard-Dialoge verwendet, die über die entsprechenden Dialog-Konstruktoren mit entsprechenden Konstanten bereitgestellt werden:

kodiereDateiFenster = new Dateizugriffe(this, "Datei kodieren",FileDialog.LOAD);
dekodiereDateiFenster = new Dateizugriffe(this, "Datei dekodieren",FileDialog.SAVE);

Abbildung 13.4:  Java stellt einen Standard-Dialog zum Öffnen von Dateien zur Verfügung, der für das Programm genutzt wird.

Abbildung 13.5:  Auch zum Speichern wird ein Standard-Dialog genutzt.

Die Dialoge stellen allerdings keine weitere Funktionalität bereit, als den Namen der ausgewählten Datei zurückzugeben. Behandeln muss man diese Information dann selbst im Programm, sowohl den Pfad dorthin als auch die Datei selbst.

PfadzuLesendeDatei = kodiereDateiFenster.getDirectory() + kodiereDateiFenster.getFile();
zuLesendeDatei = kodiereDateiFenster.getFile();

Abbildung 13.6:  Wenn eine Datei beim Speichern überschrieben werden muss, warnt ein Standard-Dialogfenster.

Wenn zwei Dateien ausgewählt wurden2, können Sie sowohl den Button zum Kodieren als auch Dekodieren auswählen. Wenn die Aktion beendet ist, wird ein selbst programmierter Ende-Dialog angezeigt.

TextDialog(Frame aufrufendesFenster, String title, boolean modal, int welcherText, String 
text) {
 super(aufrufendesFenster, title, modal);
 setLayout(new BorderLayout(10,10));
 setBackground(Color.green);
 add("South", new Button("Alles klar"));
 if (welcherText==0) {
 meinHilfeText.setEditable(false);
 add("Center",meinHilfeText);
 }
 else if (welcherText==1) {
 meinInfoText.setEditable(false);
 add("Center",meinInfoText);
 }
 else if (welcherText==2) {
 TextArea meinFehlerText = new TextArea(text, 3, 40);
 meinFehlerText.setEditable(false);
 add("North",meinFehlerText);
 }
 else if (welcherText==3) {
 meinKodierText.setEditable(false);
 add("North",meinKodierText);
 }
 else if (welcherText==4) {
 meinDekodierText.setEditable(false);
 add("North",meinDekodierText);
 } }

Abbildung 13.7:  Das Hinweisfenster am Ende der Kodierung ist selbst gestrickt.

Info- und Hilfedialog werden über ein Menü aufgerufen.

meineHilfe.add(new MenuItem("Info"));
meineHilfe.add(new MenuItem("Hilfe"));

Dabei sollte beachtet werden, dass die beiden konkreten Fenster über dieselbe selbst programmierte Klasse und denselben Konstruktor - nur mit unterschiedlichem int-Wert als viertem Parameter - erzeugt werden.

meinHilfeFenster = new TextDialog(this, "Hilfe zu RJSKRYP - Javaversion", true, 0,"");
meinInfoFenster = new TextDialog(this, "Info zu RJSKRYP - Javaversion", true, 1,"");

Die Klasse TextDialog unterscheidet auf Grund des vierten Parameters, welches Fenster zu erzeugen ist.

class TextDialog extends Dialog {
String hilfeText = "RJSKRYP für Java kodiert und dekodiert beliebige binäre Dateien."
+ "\nBedienung: \nSie wählen zunächst eine Datei als Quelle und eine als Zieldatei aus."
+ "\nAnschließend werden die Schaltflächen zum Kodieren und Dekodieren aktivierbar."
+ "\nDanach entscheiden Sie, ob Sie eine Datei ver- oder entschlüsseln wollen 
\n(entsprechende Push-Button).";
String infoText = "RJSKRYP für Java wurde entwickelt von RJS-EDV-KnowHow."
+ "\012Version 1.1"
+ "\012\012Besuchen Sie uns im Internet: "
+ "\012http://www.rjs.de";
TextArea meinHilfeText = new TextArea(hilfeText, 5, 42);
TextArea meinInfoText = new TextArea(infoText, 3, 40);
TextArea meinKodierText = new TextArea("Kodierung beendet", 3, 40);
TextArea meinDekodierText = new TextArea("Dekodierung beendet", 3, 40);
TextDialog(Frame aufrufendesFenster, String title, boolean modal, int welcherText, String 
text) {
 super(aufrufendesFenster, title, modal);
 setLayout(new BorderLayout(10,10));
 setBackground(Color.green);
 add("South", new Button("Alles klar"));
 if (welcherText==0) {
 meinHilfeText.setEditable(false);
 add("Center",meinHilfeText);
 }
 else if (welcherText==1) {
 meinInfoText.setEditable(false);
 add("Center",meinInfoText);
 }
 else if (welcherText==2) {
 TextArea meinFehlerText = new TextArea(text, 3, 40);
 meinFehlerText.setEditable(false);
 add("North",meinFehlerText);
 }
 else if (welcherText==3) {
 meinKodierText.setEditable(false);
 add("North",meinKodierText);
 }
 else if (welcherText==4) {
 meinDekodierText.setEditable(false);
 add("North",meinDekodierText);
 }   }

Abbildung 13.8:  Der Infodialog - man kann gut erkennen, dass die Schaltflächen zum Kodieren und Dekodieren aktiviert sind.

Abbildung 13.9:  Der Hilfedialog

Beenden lässt sich das Programm sowohl über einen Menüeintrag als auch über den Ende-Button.

Abbildung 13.10:  Menü und Button zum Beenden

Das Kodierungsprogramm ist so sicherlich keineswegs perfekt (was auch sicher nicht Anspruch eines Beispielprogramm sein kann). So fehlt beispielsweise eine Statusanzeige, die angibt, wie weit die Kodierung bzw. Dekodierung fortgeschritten ist.

Bei kleinen Dateien macht sich das kaum bemerkbar, aber wenn die Kodierung bzw. Dekodierung einige Zeit braucht, kann man nicht sehen, ob das Programm noch arbeitet. Auch könnten die Hinweise, Meldungen und die Hilfe erweitert werden. Das Layout ist auf jeden Fall zu verbessern. Es sollte jedoch ansonsten vollständig funktionieren und als Basis für eigene Projekte gute Dienste leisten.

13.16 Drucken unter Java

Ein wesentlicher Ausgabevorgang ist natürlich das Drucken. Das ist aber unter Java gar nicht so unproblematisch. Am einfachsten war es in Java 1.0 und dem JDK 1.0. Es ging überhaupt nicht! Das JDK 1.1 erlaubte dann ein einfaches Drucken über ein mehr schlecht als recht funktionierendes Druck-API. Gleichwohl ist es für einfache Druckaktionen immer noch sinnvoll einzusetzen. Das JDK 1.2 stellte dann endlich ein vernünftigeres Druck-API bereit. Aber auch dieses war noch nicht der Weisheit letzter Schluss. Es war insbesondere in Bezug auf Performance schlichtweg ungenügend. Ab dem JDK 1.3 hat sich das aber wesentlich verbessert, wobei die grundsätzliche Technik für den Programmierer immer noch der des JDK 1.2 entspricht. Schauen wir uns die beiden Welten an.

13.16.1 Drucken unter dem JDK 1.1

Wenn Sie unter dem JDK 1.1 drucken wollen, wird die Klasse java.awt.Toolkit und dort die Methode getPrintJob() Grundlage aller Aktionen sein. Die Methode gibt es in zwei Ausprägungen:

public abstract PrintJob getPrintJob(Frame frame, String jobtitle, Properties props)
public PrintJob getPrintJob(Frame frame, String jobtitle, JobAttributes jobAttributes, 
PageAttributes pageAttributes)

Das gesamte Verfahren ist im Prinzip relativ unkompliziert. Die Methode liefert ein Objekt des Typs PrintJob, das sämtliche Aktionen des Druckauftrags steuert. Zudem bedeutet der Aufruf dieser Methode das Öffnen eines plattformabhängigen Druckdialogs. Wenn der Anwender diesen bestätigt, wird der Druckauftrag durchgeführt. Bricht er ab, liefert die Methode den Wert null. Dieser Wert kann dann explizit verwendet werden.

Die Klasse PrintJob stellt einige Methoden bereit, die Sie für einen sinnvollen Druck benötigen.

Die Methode public abstract Graphics getGraphics() liefert ein Graphics-Objekt, das auf der nächsten Seite ausgegeben wird.

Die Methode public abstract Dimension getPageDimension() gibt die Dimension von einer Seite in Pixel zurück. Die Auflösung einer Seite wird so gewählt, dass sie der Bildschirmauflösung entspricht.

Die Methode public abstract int getPageResolution() gibt die Auflösung der Seite in Pixels per Inch zurück. Das hat aber nichts mit der Auflösung des Druckers zu tun.

Um in umgekehrter Reihenfolge zu drucken, gibt es die Methode public abstract boolean lastPageFirst().

Ganz wichtig ist die Methode public abstract void end(), womit jeder Druckauftrag beendet wird.

Um nun die Druckausgabe in einem Programm zu platzieren, gibt es zwei sinnvolle Stellen. Sie können zum einen eine eigene Methode erstellen, die die notwendigen Anweisungen integriert. Es gibt über die Klasse java.awt.Component die Methoden public void print(Graphics g) und public void printAll(Graphics g).

Die Methode print() druckt eine Komponente, printAll() eine Komponte samt den Subkomponenten. Meist ist es jedoch einfacher, die in der Regel auch für Bildschirmausgaben verwendete paint()-Methode zu gebrauchen, zumal die print()-Methoden diese implizit verwenden. Dabei werden entweder gewisse Druckaufträge automatisch ausgelöst oder so Sie rufen ein repaint() auf, wenn Sie drucken wollen. Gehen wir in die Praxis.

import java.awt.*;
import java.awt.event.*;
import java.awt.Graphics;
public class Textdruck1 extends Frame {
 public static void main(String[] args) {
 Textdruck1 wnd = new Textdruck1();
 }
 public Textdruck1() {
 super("Textdruck unter dem JDK 1.1");
 addWindowListener(
 new WindowAdapter()  {
  public void windowClosing(WindowEvent event) {
   System.exit(0);  }  }  );
 setSize(400,400);
 setVisible(true);
}
public void paint(Graphics g) {
 PrintJob pjob = getToolkit().getPrintJob(this,"Testseite",null);
 if (pjob != null) {
 g = pjob.getGraphics();
 if (g != null) {
  g.setFont(new Font("TimesRoman",Font.BOLD,24));
  g.drawString(
  "Drucken unter dem JDK 1.1",40,100);
  g.dispose();
 }
 pjob.end();
}  }  }

Abbildung 13.11:  Ein plattformabhängiger Standard-Druckdialog mit ausgewähltem Netzwerkdrucker

Das Programm ist bewusst auf die wesentlichen Aspekte reduziert. Beim Start wird einfach ein Frame geöffnet und automatisch die paint()-Methode aufgerufen. Diese ruft getPrintJob() auf und erzeugt damit ein Objekt des Typs PrintJob sowie das Öffnen eines plattformabhängigen Druckdialogs. Wenn der Anwender diesen bestätigt, wird der Druckauftrag durchgeführt. Bricht es ab, liefert die Methode den Wert null und der Druck unterbleibt. Zur konkreten Ausgabe haben wir einfach mit der auch auf dem Bildschirm verwendbaren Methode drawString() gearbeitet.

Erweitern wir das Beispiel ein wenig. Wir schaffen eine grafische Oberfläche mit einem Texteingabefeld. Dessen Inhalt soll bei einem Klick auf einen Button ausgegeben werden. Außerdem soll der Druckdialog nicht ausgelöst werden, wenn das Programm startet. Das fangen wir ganz einfach ab, indem eine boolesche Variable beim Start auf true gesetzt wird. Die paint()-Methode löst den Druck nur aus, wenn diese auf false gesetzt ist. Dies geschieht am Ende der paint()-Methode. Das bedeutet, ein nachfolgender Aufruf repaint() startet den Druck bzw. den Druckdialog. Die Methode wird beim Klick auf den entsprechenden Button ausgelöst. Beachten Sie aber, dass auch andere Ereignisse diese Methode aufrufen. Etwa wenn Sie das Fenster in den Hintergrund und dann wieder nach vorne bringen. Das Beispiel ist also nur begrenzt praxisfähig. Das soll aber auch nicht Sinn und Zweck sein.

import java.awt.*;
import java.awt.event.*;
import java.awt.Graphics;
public class Textdruck2 extends Frame {
 // Variablendeklaration
 private Panel panel1;
 private Button button1;
 private Button button2;
 private TextField textFeld1;
 private boolean start=true;
 public Textdruck2() {
 initComponents ();
 pack ();
 }
 private void initComponents() {
 panel1 = new Panel();
 button1 = new Button();
 button2 = new Button();
 textFeld1 = new TextField();
 addWindowListener(new WindowAdapter() {
 public void windowClosing(WindowEvent evt) {
 exitForm(evt);  }  }  );
 panel1.setFont(new Font ("Dialog", 0, 11));
 panel1.setName("panel1");
 panel1.setBackground(new Color (204, 204, 204));
 panel1.setForeground(Color.black);
 button1.setFont(new Font ("Dialog", 0, 11));
 button1.setLabel("Ende");
 button1.setName("button1");
 button1.setBackground(Color.lightGray);
 button1.setForeground(Color.black);
 button1.addActionListener(new ActionListener() {
 public void actionPerformed(ActionEvent evt) {
 button1ActionPerformed(evt);  }  }  );
 panel1.add(button1);
 button2.setFont(new Font ("Dialog", 0, 11));
 button2.setLabel("Drucken");
 button2.setName("button2");
 button2.setBackground(Color.lightGray);
 button2.setForeground(Color.black);
 button2.addActionListener(new ActionListener() {
 public void actionPerformed(ActionEvent evt) {
 button2ActionPerformed(evt);  }  }  );
 panel1.add(button2);
 add(panel1, BorderLayout.SOUTH);
 textFeld1.setBackground(Color.yellow);
 textFeld1.setName("text1");
 textFeld1.setFont(new Font ("Dialog", 0, 11));
 textFeld1.setForeground(Color.blue);
 add(textFeld1, BorderLayout.NORTH);
 }
private void button2ActionPerformed(ActionEvent evt) {
repaint();
 }
private void button1ActionPerformed(ActionEvent evt) {
System.exit(0);
 }
 /** Exit */
 private void exitForm(WindowEvent evt) {
 System.exit (0);
 }
 public static void main (String args[]) {
 new Textdruck2 ().show ();
 }
public void paint(Graphics g) {
 if(!start) {
 PrintJob pjob = getToolkit().getPrintJob(this,"Testseite",null);
 if (pjob != null) {
 g = pjob.getGraphics();
 if (g != null) {
 g.setFont(new Font("TimesRoman",Font.BOLD,24));
 g.drawString(textFeld1.getText(),40,100);
 g.dispose();
 }
 pjob.end();
 }
 }
 start=false;
}  }

Abbildung 13.12:  Der Inhalt des Textfeldes wird beim Klick auf den Button gedruckt.

13.16.2 Drucken unter dem SDK 2

Das Drucken unter dem SDK 2 funktioniert über einen neuen Ansatz. Er basiert auf dem Paket java.awt.print. Dort gibt es die Klasse PrinterJob. Trotz des ähnlichen Namens hat sie aber nicht die gleiche Bedeutung wie PrintJob. Sie ist nur für die Kontrolle des Drucks (Aufrufen von Dialogen, Seitenkontrolle, Start des Drucks usw.) verantwortlich. Der eigentliche Ausdruck erfolgt über die Schnittstellen Printable, PrinterGraphics oder Pageable. Deren Methoden müssen bei Bedarf überschrieben werden. Dazu stellt das Paket noch die Klassen Book (Unterstützung verschiedener Seitenformate), PageFormat (Größe und Ausrichtugn einer Seite) und Paper (physikalische Charakteristika des Papiers) zur Verfügung.

Die Klasse PrinterJob stellt eine ganze Reihe von Methoden zur Verfügung, etwa die Methode public static PrinterJob getPrinterJob(), die einen PrinterJob erstellt und ihn zurückgibt. Über public abstract void setPageable(Pageable document) throws NullPointerException kann die Anzahl der Seiten und das Seitenformat gesetzt werden. Im SDK 2 stehen nunmehr zwei Konfigurationsdialoge zur Verfügung. Der eine lässt die Einstellung von der Anzahl der Kopien, die auszudruckenden Seiten und des Druckers zu. Der andere kümmert sich um Seitenparameter. Die Methoden public abstract boolean printDialog() und public abstract PageFormat pageDialog(PageFormat page) erzeugen Dialoge zum Anpassen der Eigenschaften des Druckjobs und der Seitenformate. Die Methode public abstract void print() throws PrinterException druckt einen Satz von Seiten, public abstract void setCopies(int copies) gibt die Anzahl der Kopien an, public abstract int getCopies() fragt sie ab. Über public abstract void cancel() kann man einen Druckjob abbrechen und public abstract boolean isCancelled() kontrolliert diesen Status.

Wenn nun konkret eine Druckaktion laufen soll, verwendet man beispielsweise die Schnittstelle Printable und überschreibt die Methode public abstract void print() throws PrinterException. Von besonderem Interesse sind die in der Schnittstelle vorhandenen Konstanten PAGE_EXISTS und NO_SUCH_PAGE, auf die man in der print()-Methode abprüfen kann. Solange der Rückgabewert der Methode PAGE_EXISTS entspricht, wird immer wieder eine Seite gedruckt. Erst bei Übereinstimmung mit NO_SUCH_PAGE wird der Druckjob beendet. Schauen wir uns das in der Praxis an. Dazu nehmen wir ein einfaches Beispiel, das nur einen konstanten Text samt einer Seitenzahl ausgibt. Die Seite wird zweimal ausgegeben. Beachten Sie, dass die print()-Methode Einstellungen des Konfigurationsdialogs und auch die Angaben über Kopien für die Seite berücksichtigt. Für den konkreten Ausdruck verwenden wir in der print()-Methode Casting eines Graphics-Objekts auf ein Graphics2D-Objekt. Damit haben wir dann auch die Möglichkeiten von Java 2D zum Drucken zur Verfügung.

import java.awt.*;
import java.awt.print.*;
import java.io.*;
public class Textdruck3 implements Printable {
 // Konstanten
 private static final int SKALIERFAKTOR = 4;
 // Variablen
 private PrinterJob pjob;
 private PageFormat seitenFormat;
 private String druckText;
 // Konstruktor
 public Textdruck3(String druckText) {
 this.pjob = PrinterJob.getPrinterJob();
 this.druckText = druckText;
 }
 // Öffentliche Methoden
 public boolean setupPageFormat() {
 PageFormat defaultPF = pjob.defaultPage();
 this.seitenFormat = pjob.pageDialog(defaultPF);
 pjob.setPrintable(this, this.seitenFormat);
 return (this.seitenFormat != defaultPF);
 }
 public boolean setupJobOptions() {
 return pjob.printDialog();
 }
 public void drucke() throws PrinterException, IOException {
 pjob.print(); 
 }
 // Implementierung von Printable
 // Die Methode print() muss überschrieben 
 // werden
 public int print(Graphics g, PageFormat pf, int page) throws PrinterException {
 int druckErgeb = PAGE_EXISTS;
 // Abbruch des Druckjobs, wenn Seitenzahl
 // > 1, d.h. zwei Seite wurden gedruckt
 if(page>1) return NO_SUCH_PAGE;
 String line = null;
 Graphics2D g2 = (Graphics2D)g; 
 g2.scale(1.0 / SKALIERFAKTOR, 1.0 / SKALIERFAKTOR);
 int ypos = (int)pf.getImageableY() * SKALIERFAKTOR;
 int xpos = ((int)pf.getImageableX() + 2) * SKALIERFAKTOR;
 int yd = 12 * SKALIERFAKTOR;
 int ymax = ypos + (int)pf.getImageableHeight() * SKALIERFAKTOR - yd;
 //Seitentitel ausgeben
 ypos += yd; 
 g2.setColor(Color.black);
 g2.setFont(new Font("Monospaced", Font.ITALIC, 12 * SKALIERFAKTOR));
 g.drawString("Seite " + (page + 1), xpos, ypos);
 g.drawString(druckText, xpos, ypos*2);
 // Erneuter Aufruf von print()
 return druckErgeb;
 }
 public static void main(String[] args) {
 Textdruck3 druckProgr = new Textdruck3("Hello World");
 if (druckProgr.setupPageFormat()) {
 if (druckProgr.setupJobOptions()) {
 try {
 druckProgr.drucke();
 } 
 catch (Exception e) {
 System.err.println(e.toString());
 System.exit(1);
 }
 System.exit(0);
 }
 }
 }  }

Abbildung 13.13:  Ein plattformabhängiger Standard-Dialog zur Einstellung der Seiten-Eckdaten

Abbildung 13.14:  Ein plattformabhängiger Standard-Druckdialog zur Einstellung der konkreten Seiten, der Kopien sowie des Druckers

13.17 Zusammenfassung

Ein- und Ausgabeoperationen zählen zu den grundlegendsten Aktionen, die von einem Programm bewerkstelligt werden. In Java werden diese Ein- und Ausgabeoperationen mittels Datenströmen realisiert - einem nicht-interpretierten Strom von Bytes. Ein Datenstrom kann von jeder beliebigen Quelle kommen, d.h., der Ursprungsort spielt überhaupt keine Rolle. Die Information über die Quelle steckt in einem Strom-Argument. Außerdem gibt es noch ein weiteres Argument, das angibt, in welches Zielobjekt die verarbeiteten Daten zurückgeschrieben werden sollen.

Unter Java stehen zur Behandlung des Datenstrom-Modells zwei abstrakte Klassen zur Verfügung:

InputStream
OutputStream

Die beiden Klassen gehören zu dem Paket java.io. Die meisten Klassen zur Ein- und Ausgabe leiten sich von InputStream und OutputStream ab oder verwenden sie. Es gibt aber auch Klassen, die nicht auf diese beiden abstrakten Klassen zurückgehen. Eine der wohl wichtigsten Nicht-Streamklassen ist die Klasse File, die Dateinamen und Verzeichnisnamen in einer plattformunabhängigen Weise verwaltet.

Die einfachen Strom-Methoden erlauben nur das Versenden von Bytes mittels Datenströme. Zum Senden verschiedener Datentypen gibt es die Schnittstellen DataInput und DataOutput. Sie legen Methoden zum Senden und Empfangen anderer Java-Datentypen fest. Mithilfe der Schnittstellen ObjectInput und ObjectOutput lassen sich ganze Objekte über einen Strom zusenden. Mit dem StreamTokenizer können Sie einen Strom wie eine Gruppe von Worten behandeln. Er ist dem StringTokenizer ähnlich, der das Gleiche mit Zeichenketten tut. Für die Kommunikation von einzelnen Threads gibt es die beiden Klassen PipedInputStream zum Lesen von Daten aus einem PipedOutputStream. Der PipedOutputStream dient also zum Schreiben in einen PipedInputStream.

Alle Methoden, die sich mit Eingabe- und Ausgabeoperationen beschäftigen, werden in der Regel mit der throw IOExceptions oder aber EOFException abgesichert.

Ein wesentlicher Ausgabevorgang ist das Drucken, das in Java mit zwei unterschiedlichen Konzepte realisiert wird. Ab dem JDK 1.1 wurde das Konzept mit der Klasse PrintJob aus java.awt.Toolkit realisert, ab dem SDK 2 verlagerte man die Druckfunktionalität in das Paket java.awt.print.

1

Random Access kennen Sie wahrscheinlich aus dem Begriff RAM für den Hauptspeicher - die mehr schlechte als rechte Übersetzung für Random Access ist »wahlfreier Zugriff«.

2

Das Programm kontrolliert nicht, ob Sie korrekt die Quelle ausgewählt haben, also ob eine zu dekodierende Datei auch wirklich kodierten Code enthält.


© Copyright Markt+Technik Verlag, ein Imprint der Pearson Education Deutschland GmbH
Elektronische Fassung des Titels: Java 2 Kompendium, ISBN: 3-8272-6039-6 Kapitel: 13 Ein- und Ausgabe in Java