Programmieren in Java: Aufbau


Kapitel 4: Streams


Inhaltsverzeichnis

Dieses Buch ist unter einer Creative Commons-Lizenz lizensiert.


4.1 Allgemeines

Rohre im Computer

Streams werden im Deutschen normalerweise als Datenströme bezeichnet. Es handelt sich hierbei um ein Konzept, das auch in anderen objektorientierten Programmiersprachen wie C++ bekannt ist und angewendet wird.

Streams werden verwendet, um Daten in ein Programm einzulesen bzw. Daten aus einem Programm auszugeben. Wann immer ein Programm mit seiner Umgebung kommunizieren möchte, kann dies über Streams geschehen. Für vielfältige Datenein- und -ausgabeoperationen müssen in Java Streams verwendet werden, weil keine anderen Möglichkeiten zur Verfügung stehen, ohne Streams die gewünschten Operationen auszuführen.

Stellen Sie sich Streams einfach als Rohre vor, die von einem Programm in die Programmumgebung führen und über die Daten zwischen dem Programm und der Programmumgebung ausgetauscht werden können. Die Standardbibliothek in Java bietet zahlreiche Rohre an, die in Form von Klassen im Paket java.io definiert sind. Der Paketname io steht für Input/Output, also die Datenein- und -ausgabe.

So gibt es beispielsweise Rohre, um Daten von einer Datei in ein Programm einzulesen oder auch umgekehrt Daten in eine Datei zu schreiben. Es gibt Rohre, die das Einlesen von Daten bzw. das Ausgeben von Daten optimieren und die Performance des Programms verbessern. Man kann solche unterstützenden Rohre mit anderen Rohren verbinden und diese Rohre zusammenstecken, um die Datenein- und -ausgabe möglichst effizient zu machen.

In diesem Kapitel erhalten Sie einen Überblick über die seit Java 1.1 existierenden Streams und erfahren, wie Sie Daten in ein Programm einlesen und ausgeben.

Beachten Sie, dass Benutzerschnittstellen, die mit AWT oder Swing entwickelt sind, zwar auch eine Form der Datenein- und -ausgabe ermöglichen, diese jedoch nicht zu den Streams gezählt wird. Während AWT und Swing das Augenmerk auf eine übersichtliche Präsentation von Daten innerhalb der Programmoberfläche legen, sind Streams grundlegendere Mechanismen zur Datenein- und -ausgabe, die in keinster Weise mit AWT und Swing vergleichbar sind und auch nicht in Konkurrenz zu diesen stehen.


4.2 Übersicht

Zeichen- und byte­orientierte Streams für Ein- und Ausgabe mit grundlegenden oder unterstützenden Funktionen

Streams können nach drei verschiedenen Kriterien gruppiert werden: Streams sind entweder zeichen- oder byteorientiert, Streams unterstützen entweder die Ein- oder Ausgabe, und Streams führen entweder grundlegende Ein- und Ausgabeoperationen oder aber lediglich unterstützende Aufgaben aus.

Java unterscheidet zeichen- und byteorientierte Streams. Zeichenorientierte Streams werden verwendet, wenn Buchstaben, Wörter oder Texte in irgendeiner Form zwischen einem Programm und seiner Umgebung ausgetauscht werden müssen. Byteorientierte Streams werden für alle anderen Arten von Datenaustauschvorgängen verwendet, also wenn zum Beispiel Grafiken oder Objekte ausgetauscht werden sollen. Der Grund für diese Unterscheidung ist, dass Zeichen in Java auf 2 Bytes basieren, Rohdaten, wie sie für sämtliche anderen Datenformate verwendet werden, jedoch jeweils auf 1 Byte.

Die Unterscheidung in zeichen- und byteorientierte Streams macht es zum Beispiel möglich, dass eine Anwendung Texte, die auf einem beliebigen Zeichensatz basieren, richtig in Dateien speichern kann, wenn der Speichervorgang mit einem zeichenorientierten Stream stattfindet. Soll zum Beispiel ein Text bestehend aus chinesischen Schriftzeichen in einer Datei gespeichert werden, so weiß der verwendete zeichenorientierte Stream automatisch, wie die chinesischen Zeichen gespeichert werden müssen. Die Unterscheidung in zeichen- und byteorientierte Streams vereinfacht also die Entwicklung von Anwendungen, die weltweit eingesetzt werden können, unabhängig von in verschiedenen Ländern üblicherweise eingesetzten Zeichensätzen.

In Java 1.0 gab es übrigens nur byteorientierte Streams. Jeder Java-Entwickler hätte also selber die Funktionalität implementieren müssen, die seit Java 1.1 durch die zeichenorientierten Streams der Standardbibliothek zur Verfügung steht.

Selbst wenn Ihnen der Grund für die Unterscheidung in zeichen- und byteorientierte Streams nicht völlig klar sein sollte, so können Sie nichts falsch machen, wenn Sie für Buchstaben, Wörter, Sätze und beliebige Texte immer die zeichenorientierten Streams verwenden und für sämtliche anderen Daten die byteorientierten Streams.

Die Unterscheidung in zeichen- und byteorientierte Streams wird anhand der Klassen in der Standardbibliothek deutlich. Zeichenorientierte Streams sind in Form von Klassen implementiert, die alle entweder von der Klasse java.io.Reader oder von der Klasse java.io.Writer abgeleitet sind. Alle Klassen, die byteorientierte Streams implementieren, sind Kindklassen von java.io.InputStream oder von java.io.OutputStream.

An dieser Stelle wird auch das nächste Unterscheidungsmerkmal für Streams sichtbar. Streams können nicht nur nach Zeichen- oder Byteorientierung in zwei Gruppen eingeteilt werden, sondern sie unterscheiden sich auch danach, ob sie Daten in ein Programm einlesen oder Daten aus einem Programm ausgeben. Alle Klassen, die von java.io.Reader und java.io.InputStream abgeleitet sind, lesen Daten in ein Programm ein - einmal zeichenorientiert, einmal byteorientiert. Alle Klassen, die von java.io.Writer und java.io.OutputStream abgeleitet sind, geben Daten aus einem Programm aus - wiederum einmal zeichenorientiert, einmal byteorientiert.

Selbstverständlich gibt es die üblichen Ausnahmen, nämlich Streams, die sowohl Daten einlesen als auch ausgeben können. So gibt es zum Beispiel seit der allerersten Java-Version 1.0 eine Klasse java.io.RandomAccessFile, die Daten aus Dateien lesen als auch in Dateien speichern kann. In diesem Fall würden also nicht zwei Streams benötigt werden, die getrennt Daten lesen und schreiben, sondern es würde ein Objekt vom Typ java.io.RandomAccessFile reichen.

Streams können also danach unterschieden werden, ob Daten zeichen- oder byteorientiert verarbeitet werden. Sie können außerdem danach unterschieden werden, ob Daten gelesen oder ausgegeben werden. Eine dritte und letzte Unterscheidungsmöglichkeit bezieht sich darauf, ob Streams grundlegende Ein- und Ausgabetätigkeiten durchführen oder lediglich unterstützende Hilfeleistung bieten, um andere grundlegende Streams zum Beispiel effizienter arbeiten zu lassen. Dieses Unterscheidungsmerkmal wird Ihnen im Abschnitt 4.5, „Verketten von Streams“ vorgestellt, nachdem Sie in den nächsten beiden Abschnitten eine Übersicht über die seit Java 1.1 existierenden Streams erhalten haben.


4.3 Zeichenorientierte Streams

Ein- und Ausgabe von Buchstaben, Wörtern und Texten

Im Folgenden werden Ihnen alle Stream-Klassen vorgestellt, mit denen Daten zeichenorientiert in Java gelesen und ausgegeben werden können. Folgende erste Übersicht enthält alle Klassen, die von java.io.Reader abgeleitet sind und daher ein zeichenweises Einlesen von Daten in ein Programm ermöglichen.

  • java.io.BufferedReader stellt eine gepufferte und daher effizientere Eingabemöglichkeit zur Verfügung. Dieser Stream dient also lediglich einer optimierten Eingabe.

  • java.io.LineNumberReader ist von java.io.BufferedReader abgeleitet und bietet die Möglichkeit, die gelesenen Zeilen mitzuzählen.

  • java.io.CharArrayReader wird verwendet, um ein Array bestehend aus Zeichen zu erstellen, das als zu lesende Datenquelle verwendet werden kann. Genaugenommen werden mit diesem Stream also keine Zeichen zwischen dem Programm und seiner Umgebung ausgetauscht, sondern das Programm behandelt das mit dieser Klasse erstellte Array so als wäre es eine Datenquelle aus der Programmumgebung.

  • java.io.InputStreamReader liest Zeichen ein, die auf 1 Byte basieren, und wandelt sie in Zeichen um, die wie in Java üblich auf 2 Bytes basieren. Wann immer Sie also Zeichen aus der Programmumgebung einlesen müssen und diese Zeichen von Java-fremden Anwendungen zur Verfügung gestellt werden, bei denen Zeichen nur auf 1 Byte basieren, so werden mit Hilfe dieser Klasse derartige Java-fremde Zeichen in die für Java übliche Darstellung von Zeichen umgewandelt.

  • java.io.FileReader ist von java.io.InputStreamReader abgeleitet und wird verwendet, um Zeichen aus Dateien in ein Programm einzulesen. Da Textdateien normalerweise so gespeichert sind, dass Zeichen auf 1 Byte basieren, wandelt diese Klasse automatisch die aus der Datei gelesenen Zeichen so um, dass sie wie in Java üblich auf 2 Bytes basieren.

  • java.io.PushbackReader ist von java.io.FilterReader abgeleitet und ermöglicht es, aus dem Stream gelesene Zeichen oder beliebige andere Zeichen zurück in den Stream zu stellen, so dass nachfolgende Lesevorgänge die zurückgestellten Zeichen zurückgeben. Die Elternklasse java.io.FilterReader wird nicht explizit vorgestellt, da sie abstrakt ist.

  • java.io.PipedReader ist eine Klasse, um Zeichen zu lesen, die mit java.io.PipedWriter geschrieben werden. Eine Anwendung kann von diesen beiden Klassen Objekte erstellen und diese Objekte jeweils zu einem Paar verknüpfen. Zeichen, die in Objekte geschrieben werden, die auf java.io.PipedWriter basieren, können dann über das entsprechende auf java.io.PipedReader basierende Objekt gelesen werden, das mit dem ersten Objekt verknüpft ist.

  • java.io.StringReader ermöglicht es, eine Zeichenkette vom Typ java.lang.String als Datenquelle zu nutzen. Ähnlich wie java.io.CharArrayReader werden auch in diesem Fall keine Daten aus der Programmumgebung gelesen, sondern ein herkömmlicher String als Datenquelle betrachten.

Die folgende Übersicht stellt Ihnen alle Klassen vor, die von java.io.Writer abgeleitet sind und Gegenstücke zu den eben vorgestellten Klassen darstellen. Mit den folgenden Klassen können Zeichen von einem Programm geschrieben werden.

  • java.io.BufferedWriter verwenden Sie, um Zeichen gepuffert auszugeben. Der einzige Sinn und Zweck dieser Klasse und der Pufferung ist, dass auf diese Weise eine Datenausgabe effizienter stattfindet.

  • java.io.CharArrayWriter ermöglicht es Ihnen, ein Array vom Typ char als Datensenke zu verwenden. Sie tauschen also keine Zeichen mit der Programmumgebung aus, sondern schreiben Zeichen in ein Array.

  • java.io.OutputStreamWriter wandelt Zeichen, die auf 2 Bytes basieren, in Zeichen um, die auf 1 Byte basieren. Sie setzen diese Klasse ein, wenn Sie Zeichen aus Java in die Programmumgebung schreiben wollen, die aus Anwendungen besteht, in denen Zeichen auf 1 Byte basieren. Diese Klasse führt also eine Umwandlung von Zeichen in Java, die auf 2 Bytes basieren, hinüber zu Zeichen aus, die auf 1 Byte basieren.

  • java.io.FileWriter wird verwendet, um Zeichen in eine Datei zu schreiben. Diese Klasse ist von java.io.OutputStreamWriter abgeleitet, so dass automatisch eine Umwandlung von Zeichen in Java, die auf 2 Bytes basieren, in Zeichen stattfindet, die auf 1 Byte basieren.

  • java.io.PrintWriter bietet zahlreiche Methoden an, um Informationen verschiedenster Datentypen auszugeben. Diese Klasse vereinfacht letztendlich die Datenausgabe, da zum Beispiel Wahrheitswerte direkt über eine Methode geschrieben werden können und nicht erst vom Programmierer in irgendeine Zeichenkette umgewandelt werden müssen.

  • java.io.PipedWriter verwenden Sie im Zusammenhang mit java.io.PipedReader. Sie können jeweils zwei Objekte, die auf diesen beiden Klassen basieren, verknüpfen. Alle Zeichen, die Sie in das Objekt vom Typ java.io.PipedWriter hineinschreiben, werden über das verknüpfte Objekt vom Typ java.io.PipedReader gelesen.

  • java.io.StringWriter macht es möglich, Zeichen in ein Objekt vom Typ java.lang.String zu schreiben. Ähnlich wie bei java.io.CharArrayWriter werden auch hier keine Zeichen in die Programmumgebung geschrieben, sondern ein Objekt im Programm wird als Datensenke verwendet.

Die Klasse java.io.FilterWriter zählt zwar auch zu den zeichenorientierten Streams zur Datenausgabe, wird aber nicht explizit vorgestellt, da sie abstrakt ist.


4.4 Byteorientierte Streams

Ein- und Ausgabe beliebiger Daten

Nachdem Ihnen die Klassen für die zeichenorientierte Ein- und Ausgabe vorgestellt wurden, erhalten Sie im Folgenden einen Überblick über die byteorientierten Streams. Die folgende Übersicht enthält alle Klassen, um Daten byteorientiert in ein Programm einzulesen. Folgende Klassen sind alle von java.io.InputStream abgeleitet.

  • java.io.BufferedInputStream verwenden Sie zur Performance-Steigerung. Diese Klasse stellt einen Puffer dar, über den Daten effizienter eingelesen werden können.

  • java.io.ByteArrayInputStream stellt ein Array vom Typ byte dar, das als Datenquelle verwendet werden kann. Der Datenaustausch findet also bei diesem Stream nicht mit der Programmumgebung statt, sondern mit einem Objekt dieser Klasse, das direkt im Programm zur Verfügung steht.

  • java.io.FileInputStream ermöglicht es, Daten aus Dateien zu lesen.

  • java.io.FilterInputStream macht es möglich, Daten beim Einlesen aus einer Datenquelle zu filtern oder in einer bestimmten Weise zu verarbeiten. Von dieser Klasse gibt es eine ganze Reihe von Kindklassen, die bestimmte Verarbeitungsformen anbieten. So existiert zum Beispiel seit Java 1.4 die Klasse java.io.CipherInputStream, die eine Kindklasse von java.io.FilterInputStream ist und über die Daten beim Einlesen entschlüsselt werden können, die beim Schreiben mit java.io.CipherOutputStream verschlüsselt wurden.

  • java.io.PushbackInputStream ist eine Kindklasse von java.io.FilterInputStream. Diese Klasse ermöglicht es, gelesene Daten oder beliebige andere Daten in den Stream zurückzustellen, so dass sie bei nachfolgenden Lesevorgängen zurückgegeben werden.

  • java.io.PipedInputStream wird im Zusammenhang mit java.io.PipedOutputStream verwendet. Wenn Sie Objekte vom Typ dieser beiden Klassen erstellen, können Sie jeweils zwei Objekte paarweise verknüpfen. Daten, die in Objekte vom Typ java.io.PipedOutputStream geschrieben werden, werden dann im Objekt vom Typ java.io.PipedInputStream gelesen.

Die Klassen java.io.LineNumberInputStream und java.io.StringBufferInputStream, die ebenfalls zu den byteorientierten Klassen zur Dateneingabe zählen, sind in der Java-Dokumentation der Standardbibliothek als deprecated gekennzeichnet. Das bedeutet, dass diese Klassen nicht verwendet werden sollten und sie in zukünftigen Java-Versionen möglicherweise nicht mehr enthalten sein werden. Beide Klassen arbeiten unter Umständen nicht korrekt, da sie Annahmen über den zu lesenden Datenstrom machen, die eventuell nicht zutreffen.

Die eigentliche Aufgabe der Klasse java.io.LineNumberInputStream, gelesene Zeilen zu zählen, sollte daher mit java.io.LineNumberReader bewerkstelligt werden. Anstatt Daten über ein Objekt vom Typ java.io.StringBufferInputStream aus einem String zu lesen, sollte die Klasse java.io.StringReader verwendet werden.

Die folgende Übersicht stellt Ihnen abschließend die Klassen vor, die von java.io.OutputStream abgeleitet sind. Diese Klassen ermöglichen ein byteorientiertes Schreiben von Daten.

  • java.io.BufferedOutputStream existiert allein aus Performance-Gründen. Mit Hilfe dieser Klasse finden Schreibvorgänge von Daten gepuffert und damit effizienter statt.

  • java.io.ByteArrayOutputStream ermöglicht es Ihnen, ein Array vom Typ byte als Datensenke zu verwenden. Dieser Stream führt also nicht in die Programmumgebung, sondern Daten, die in diesen Stream geschrieben werden, werden in einem Array innerhalb des Programms gespeichert.

  • java.io.FilterOutputStream gestattet eine Filterung oder Verarbeitung von Daten während des Schreibvorgangs. In der Standardbibliothek existieren eine ganze Reihe von Klassen, die von dieser Klasse abgeleitet sind. So ermöglicht die seit Java 1.4 existierende Kindklasse java.io.CipherOutputStream die Verschlüsselung von Daten bei einem Schreibvorgang.

  • java.io.FileOutputStream verwenden Sie, wenn Sie Daten in eine Datei hineinschreiben möchten.

  • java.io.PrintStream vereinfacht viele Schreibvorgänge, da diese Klasse zahlreiche Methoden anbietet, um Informationen verschiedenster Datentypen auszugeben.

  • java.io.PipedOutputStream verwenden Sie im Zusammenhang mit java.io.PipedInputStream. Objekte dieser beiden Datentypen können paarweise verknüpft werden. Daten, die in Objekte vom Typ java.io.PipedOutputStream geschrieben werden, werden über das paarweise verknüpfte Objekt vom Typ java.io.PipedInputStream gelesen.

Sie werden in der Java-Dokumentation der Standardbibliothek auf weitere Stream-Klassen für byteorientierte Operationen stoßen, die aber in diesem Kapitel nicht weiter vorgestellt werden.


4.5 Verketten von Streams

Rohre zusammenstecken und neue bessere Rohre bauen

Wenn Sie sich zum Beispiel die Klassen für zeichenorientiertes Lesen ansehen, die von java.io.Reader abgeleitet sind und im Abschnitt 4.3, „Zeichenorientierte Streams“ ausführlich vorgestellt wurden, so stellen Sie fest, dass einige Klassen grundlegende Leseoperationen ausführen und tatsächlich Zeichen aus einer Datenquelle lesen, während andere Kindklassen von java.io.Reader lediglich Verarbeitungsroutinen für Streams zur Verfügung stellen, ohne tatsächlich Zeichen einer spezifizierten Datenquelle zu entnehmen.

Die Klassen java.io.CharArrayReader, java.io.FileReader, java.io.PipedReader und java.io.StringReader gehören zu den Streams, die tatsächlich Zeichen aus einer ganz konkreten Datenquelle lesen. Jede dieser Klassen wird verwendet, um einen Stream mit einer speziellen Datenquelle zu verbinden - sei es ein Array vom Typ char, eine Datei, eine Pipe oder ein String.

Dahingehend gehören die Klassen java.io.BufferedReader, java.io.LineNumberReader, java.io.InputStreamReader und java.io.PushbackReader zu den unterstützenden Streams. Sie werden nicht verwendet, um Zeichen aus einer bestimmten Datenquelle zu lesen, sondern sie werden verwendet, um grundlegende Streams beim Lesen zu unterstützen - sei es durch Pufferung für effizientere Lesevorgänge, durch das Zählen gelesener Zeilen, durch die Umwandlung von Zeichen von 1 Byte in 2 Bytes oder durch die Möglichkeit, Zeichen in den Stream zurückzustellen.

Diese Unterscheidung in grundlegende Streams und unterstützende Streams ist nicht nur bei den zeichenorientierten lesenden Klassen möglich, sondern auch bei den anderen Klassen, die ein zeichenorientiertes Schreiben bzw. ein byteorientiertes Lesen und Schreiben ermöglichen. Es handelt sich hierbei um das dritte Unterscheidungsmerkmal, nachdem Ihnen unter Abschnitt 4.2, „Übersicht“ bereits die Unterscheidungsmerkmale Zeichen- oder Byteorientierung und Ein- oder Ausgabe vorgestellt wurden.

Es ist nun möglich, grundlegende Streams und unterstützende Streams zu verketten, also die Rohre aneinanderzuhängen. Normalerweise reicht es zum Beispiel aus, in einem Programm mit folgender Zeile Zugriff auf eine Datei zu erhalten, um Zeichen aus dieser Datei zu lesen.

import java.io.*; 

FileReader file = new FileReader("brief.txt"); 

Die Lesevorgänge aus der Datei werden jedoch optimiert, wenn zusätzlich auf die Klasse java.io.BufferedReader zugegriffen wird und der durch diese Klasse zur Verfügung gestellte Puffer genutzt wird.

import java.io.*; 

FileReader file = new FileReader("brief.txt"); 
BufferedReader buffer = new BufferedReader(file); 

Indem der Stream, der den Zugriff auf die Datei brief.txt ermöglicht, gepuffert wird, finden Lesevorgänge ab sofort effizienter statt. Es darf dann im Programm natürlich auch nur mehr über den Puffer, der über die Referenzvariable buffer zur Verfügung gestellt wird, auf die Datei zugegriffen werden. Die Referenzvariable file wird daher nicht benötigt, so dass normalerweise das Verketten von Streams in einer Zeile geschieht.

import java.io.*; 

BufferedReader buffer = new BufferedReader(new FileReader("brief.txt")); 

Die Verkettung sieht so aus, dass der Stream, der direkt mit der Programmumgebung verbunden ist, als Objekt im Konstruktor an den Stream übergeben wird, der mit dem Programm verbunden ist. Während das Programm also im obigen Beispiel nur mit dem Puffer arbeiten wird, greift der Puffer in Form der Klasse java.io.BufferedReader über den Stream java.io.FileReader auf eine Datei zu, um aus dieser Zeichen zu lesen.

Es ist übrigens ohne Probleme möglich, mehr als zwei Streams zu verketten. Dies kann hin und wieder sinnvoll sein. In diesem Fall werden mehrere unterstützende Streams mit einem Stream verbunden, der grundlegende Zugriffsoperationen ausführt. Es macht keinen Sinn, mehrere grundlegende Streams zu verknüpfen, da jeder von ihnen ja mit einer anderen Datenquelle verbunden wird. Grundlegende Streams bieten daher auch gar keine entsprechenden Konstruktoren an, so dass auch praktisch solche Verknüpfungen unmöglich sind.

Die folgenden Abschnitte in diesem Kapitel stellen Ihnen verschiedene Streams detailliert vor und zeigen Ihnen in zahlreichen Beispielen, wie sie eingesetzt werden.


4.6 Dateien lesen und schreiben

Zugriff auf Dateien

Um Zeichen und Daten aus Dateien zu lesen oder in Dateien zu schreiben, greifen Sie auf die Klassen java.io.FileReader, java.io.FileWriter, java.io.FileInputStream und java.io.FileOutputStream zu. Sie verwenden java.io.FileReader und java.io.FileWriter für zeichenorientierte Zugriffe und java.io.FileInputStream und java.io.FileOutputStream für byteorientierte Zugriffe.

Im folgenden Beispielprogramm wird Ihnen gezeigt, wie Sie mit Hilfe von java.io.FileWriter einen Text in einer Datei speichern können. Hierfür wird ein Java-Programm in Form einer Application erstellt. Die Anwendung besteht aus einem Fenster, das ein Texteingabefeld, eine Schaltfläche zum Speichern und eine Textbox enthält, um Meldungen ausgeben zu können.

Beachten Sie, dass der Zugriff auf das Dateisystem in Java-Applets nicht möglich ist, da dies ein viel zu großes Sicherheitsrisiko wäre. Java-Applications laufen jedoch im Gegensatz zu Java-Applets nicht unter Beobachtung eines Security-Managers ab und haben uneingeschränkte Zugriffsrechte auf das Computer-System. Daher sind folgende Beispiele, die den Zugriff auf Dateien erläutern, alle in Form von Applications entwickelt.

import java.awt.*; 
import java.awt.event.*; 
import java.io.*; 

public class MyApplication extends Frame implements ActionListener, WindowListener 
{ 
  static MyApplication myapp; 
  static Label label; 
  static TextArea text; 
  static Button save; 

  public static void main(String[] args) 
  { 
    myapp = new MyApplication(); 
    myapp.setLayout(new BorderLayout()); 
    myapp.setSize(500, 400); 
    label = new Label(); 
    myapp.add(label, BorderLayout.NORTH); 
    text = new TextArea(); 
    myapp.add(text, BorderLayout.CENTER); 
    save = new Button("Text speichern"); 
    save.addActionListener(myapp); 
    myapp.add(save, BorderLayout.SOUTH); 
    myapp.addWindowListener(myapp); 
    myapp.setVisible(true); 
  } 

  public void actionPerformed(ActionEvent ev) 
  { 
    try 
    { 
      FileWriter file = new FileWriter("brief.txt"); 
      String message = text.getText(); 
      file.write(message, 0, message.length()); 
      file.close(); 
      label.setText("Datei wurde erfolgreich gespeichert."); 
    } 
    catch (IOException ex) 
    { 
      label.setText("Fehler: " + ex.getMessage()); 
    } 
  } 

  public void windowClosing(WindowEvent ev) 
  { 
    myapp.setVisible(false); 
    myapp.dispose(); 
    System.exit(1); 
  } 

  public void windowActivated(WindowEvent ev) { } 
  public void windowClosed(WindowEvent ev) { } 
  public void windowDeactivated(WindowEvent ev) { } 
  public void windowDeiconified(WindowEvent ev) { } 
  public void windowIconified(WindowEvent ev) { } 
  public void windowOpened(WindowEvent ev) { } 
} 

Die Klasse MyApplication ist von java.awt.Frame abgeleitet und implementiert die beiden Interfaces java.awt.event.ActionListener und java.awt.event.WindowListener, um auf Benutzereingaben reagieren zu können. Wenn der Anwender die Schaltfläche zum Speichern von Text in einer Datei anklickt, wird die Methode actionPerformed() aufgerufen. Beim Versuch, das Anwendungsfenster zu schließen, wird die Methode windowClosing() ausgeführt.

Innerhalb der Methode main() wird eine Benutzerschnittstelle erstellt. Das Beispiel greift auf den Layout Manager java.awt.BorderLayout zu, um drei Elemente in der Fensteroberfläche auszurichten: Eine Textbox vom Typ java.awt.Label für die Ausgabe von Erfolgs- und Fehlermeldungen, ein mehrzeiliges Texteingabefeld vom Typ java.awt.TextArea und eine Schaltfläche vom Typ java.awt.Button zum Speichern von Text.

Innerhalb der Methode actionPerformed() findet der Zugriff auf eine Datei mit Hilfe des Streams java.io.FileWriter statt. Wie Sie sehen ist der gesamte Code der Methode actionPerformed() in eine try-catch-Klausel gestellt worden. Der Grund ist der, dass sehr viele Operationen mit Streams fehlschlagen können. In diesem Fall wird eine Ausnahme vom Typ java.io.IOException geworfen. Sollte also aus irgendeinem Grund ein derartiger Fehler im Programm auftreten, wird er abgefangen. Im catch-Anweisungsblock wird die entsprechende Fehlermeldung in der Programmoberfläche ausgegeben, um den Anwender vom Fehlschlagen des Speichervorgangs zu informieren.

Innerhalb von actionPerformed() wird zuerst ein Objekt vom Typ java.io.FileWriter erstellt. Dem Konstruktor wird als einziger Parameter ein String übergeben. Es handelt sich hierbei um den Dateinamen. Wenn die angegebene Datei nicht existiert, wird sie neu angelegt. Anderfalls wird sie überschrieben. Seit Java 1.4 gibt es einen Konstruktor, der zusätzlich zum Dateinamen einen zweiten Parameter erwartet, über den festgelegt werden kann, ob eine Datei überschrieben oder gegebenenfalls nur um neue Zeichen erweitert werden soll.

Bereits beim Erstellen eines Objekts vom Typ java.io.FileWriter kann eine Ausnahme vom Typ java.io.IOException eintreten. Wenn zum Beispiel die Datei, deren Name als Parameter dem Konstruktor übergeben wird, bereits existiert und zusätzlich schreibgeschützt ist, kann sie nicht überschrieben werden. In diesem Fall wird eine Ausnahme vom Typ java.io.IOException geworfen.

Falls der Stream geöffnet werden konnte und keine Ausnahme eintritt, wird im nächsten Schritt über die Referenzvariable text auf das Texteingabefeld zugegriffen und der vom Anwender eingegebene Text in message gespeichert. Der in dieser Variable gespeicherte Text soll nun in die Datei geschrieben werden. Dazu wird auf den Stream zugegriffen, der über die Referenzvariable file zur Verfügung steht, und die Methode write() aufgerufen. Diese Methode bekommt drei Parameter übergeben, nämlich den zu speichernden Text in Form einer Variablen vom Typ java.lang.String, die Startposition der zu speichernden Zeichen aus dem String und die Anzahl der zu speichernden Zeichen ab der Startposition. Da in diesem Beispiel die gesamte Eingabe des Anwenders gespeichert werden soll, wird als Startposition der Wert 0 übergeben und als Anzahl zu speichernder Zeichen die Länge des Strings. Mit Hilfe von write() ist es also kein Problem, auch nur eine Teilzeichenkette aus einem String zu speichern, da über den zweiten und dritten Parameter jeweils Startposition und Anzahl der Zeichen angegeben werden können.

Nachdem write() ausgeführt wurde, wird mit Hilfe der Methode close() der Stream geschlossen. Damit ist die Ausgabe der Zeichen in die Datei beendet und abgeschlossen. Die nächste Code-Anweisung in actionPerformed() greift lediglich auf die Textbox im Fenster zu, um eine entsprechende Erfolgsmeldung auszugeben.

Das Beispielprogramm wird im Folgenden nun so erweitert, dass Text nicht nur in einer Datei gespeichert werden kann, sondern auch aus einer Datei geladen werden kann.

import java.awt.*; 
import java.awt.event.*; 
import java.io.*; 

public class MyApplication extends Frame implements ActionListener, WindowListener 
{ 
  static MyApplication myapp; 
  static Label label; 
  static TextArea text; 
  static Button save, load; 

  public static void main(String[] args) 
  { 
    myapp = new MyApplication(); 
    myapp.setLayout(new BorderLayout()); 
    myapp.setSize(500, 400); 
    label = new Label(); 
    myapp.add(label, BorderLayout.NORTH); 
    text = new TextArea(); 
    myapp.add(text, BorderLayout.CENTER); 
    Panel panel = new Panel(new GridLayout(1, 2)); 
    myapp.add(panel, BorderLayout.SOUTH); 
    save = new Button("Text speichern"); 
    save.addActionListener(myapp); 
    panel.add(save); 
    load = new Button("Text laden"); 
    load.addActionListener(myapp); 
    panel.add(load); 
    myapp.addWindowListener(myapp); 
    myapp.setVisible(true); 
  } 

  public void actionPerformed(ActionEvent ev) 
  { 
    try 
    { 
      if (ev.getSource() == save) 
      { 
        FileWriter file = new FileWriter("brief.txt"); 
        String message = text.getText(); 
        file.write(message, 0, message.length()); 
        file.close(); 
        label.setText("Datei wurde erfolgreich gespeichert."); 
      } 
      else 
      { 
        FileReader file = new FileReader("brief.txt"); 
        StringBuffer message = new StringBuffer(); 
        char c; 
        while (file.ready()) 
        { 
          c = (char)file.read(); 
          message.append(c); 
        } 
        file.close(); 
        text.setText(message.toString()); 
        label.setText("Datei wurde erfolgreich geladen."); 
      } 
    } 
    catch (IOException ex) 
    { 
      label.setText("Fehler: " + ex.getMessage()); 
    } 
  } 

  public void windowClosing(WindowEvent ev) 
  { 
    myapp.setVisible(false); 
    myapp.dispose(); 
    System.exit(1); 
  } 

  public void windowActivated(WindowEvent ev) { } 
  public void windowClosed(WindowEvent ev) { } 
  public void windowDeactivated(WindowEvent ev) { } 
  public void windowDeiconified(WindowEvent ev) { } 
  public void windowIconified(WindowEvent ev) { } 
  public void windowOpened(WindowEvent ev) { } 
} 

Der Anwendung wird innerhalb von main() zusätzlich eine Schaltfläche hinzugefügt, über die ein Text aus einer Datei geladen werden kann. Um die nun zwei existierenden Schaltflächen zum Speichern und Laden der Datei ansprechend anzuordnen, wird auf die Klasse java.awt.Panel zugegriffen, die mit Hilfe des Layout Manager java.awt.GridLayout in zwei horizontal nebeneinander liegende Zellen unterteilt wird.

Da bei einem Mausklick auf die Schaltflächen zum Speichern und Laden der Datei jedesmal die Methode actionPerformed() aufgerufen wird, wird nun zusätzlich in dieser Methode über den Parameter vom Typ java.awt.event.ActionEvent abgefragt, welche Schaltfläche jeweils genau das Ereignis ausgelöst hat. Bei einem Mausklick auf die Schaltfläche, um Text zu speichern, wird der gleiche Code wie im vorherigen Beispiel ausgeführt. Der Code, um den Ladevorgang auszuführen, ist jedoch neu.

Um Zeichen aus einer Datei zu lesen muss auf den Stream java.io.FileReader zugegriffen werden. Dem Konstruktor dieser Klasse wird, wenn ein Objekt mit new angelegt wird, der Name der Datei übergeben, die zum Lesen geöffnet werden soll. Wird die Datei nicht gefunden, tritt eine Ausnahmesituation ein. In diesem Fall wird eine Ausnahme vom Typ java.io.FileNotFoundException geworfen. Diese wird und braucht im Beispiel nicht explizit in der try-catch-Klausel abgefangen werden, da es sich um eine Kindklasse von java.io.IOException handelt.

Der Lesevorgang an sich funktioniert nun nicht so einfach wie der Schreibvorgang. Das Problem bei Streams ist im Allgemeinen, dass bei einem Zugriff auf den Stream nicht bekannt ist, ob Zeichen gelesen werden können und wenn ja, wie viele. Sie wissen also nicht, wenn Sie auf eine Datei zugreifen, ob in dieser Datei überhaupt Zeichen enthalten sind - möglicherweise ist die Datei leer - und wenn ja, wie viele Zeichen genau in dieser Datei gespeichert sind.

Der Zugriff auf Streams, um Daten zu lesen, erfolgt daher immer in einer Schleife. Sie greifen in dieser Schleife wiederholt auf den Stream zu, und zwar solange der Stream neue Zeichen zum Lesen bereithält. Ob der Stream neue Zeichen zum Lesen bereithält, erkennen Sie anhand des Rückgabewertes der Methode ready(). Diese Methode gibt einen Wert vom Typ boolean zurück - true, wenn ein neuer Leservorgang Zeichen zurückgibt, und false, wenn momentan keine neue Zeichen über den Stream gelesen werden können.

Die Methode ready() existiert in der eben vorgestellten Form für alle zeichenorienteren Streams, mit denen Daten gelesen werden können. Sie ist in der Klasse java.io.Reader definiert, von der alle zeichenorientierten Streams zum Lesen von Daten abgeleitet sind.

Wenn Sie die Methode ready() aufrufen und diese Methode als Ergebnis true zurückgibt, wissen Sie, dass Sie nun mindestens ein Zeichen aus dem Stream lesen können. Dieser Lesevorgang wird im Beispiel mit Hilfe der Methode read() ausgeführt. Diese Methode gibt einen Wert vom Typ int zurück. Möglicherweise hätten Sie einen Rückgabewert vom Typ char erwartet, da ja schließlich Zeichen eingelesen werden.

Der Grund, warum read() den Datentyp int besitzt, ist, dass diese Methode auch einen negativen Wert zurückgeben können muss. Das Ergebnis ist dann negativ, wenn beim Lesezugriff mit read() festgestellt wird, dass das Ende des Streams erreicht wurde und keine weiteren Daten mehr aus dem Stream gelesen werden können. Das Problem mit einem Rückgabewert vom Typ char wäre, dass die gesamte Bandbreite von char für irgendwelche Zeichen benötigt wird - es gäbe keine Möglichkeit, irgendeinen Wert als das Ende des Streams zu interpretieren, da sämtliche 65536 Werte, die in eine Variable vom Typ char hineinpassen, alle irgendein Zeichen definieren. Indem als nächstgrößerer Datentyp int für den Rückgabewert verwendet wird, können die 65536 möglichen positiven Werte, die in ein int hineinpassen, genau die char-Werte abbilden. Zusätzlich kann aber nun auch ein negativer Wert als Ende des Streams zurückgegeben werden.

Dieser Fall, also ein negativer Rückgabewert, kann im vorliegenden Beispielprogramm nicht auftreten, da vor dem Lesezugriff bereits mit ready() überprüft wird, ob ein Lesevorgang Zeichen zurückgeben wird. Daher wird hier einfach durch Casting der Datentyp des Rückgabewertes in char umgewandelt, und dann das Zeichen, das in der Variablen c gespeichert wurde, an message angehängt. Die Referenzvariable message muss auf ein Objekt vom Typ java.lang.StringBuffer verweisen, da nur dieser Datentyp es zulässt, einen String zu verändern, also Zeichen anzuhängen oder auch abzuschneiden. Objekte vom Typ java.lang.String sind statisch und können nicht nachträglich geändert werden.

Wenn die Schleife beendet wird, weil ready() als Ergebnis false zurückgibt, ist in der Variablen message der Inhalt der Datei enthalten. Da der Stream nicht mehr benötigt wird, wird er mit close() geschlossen. Danach wird für message die Methode toString() aufgerufen, die ein Objekt vom Typ java.lang.String zurückgibt, das wiederum als Parameter an die Methode setText() übergeben wird, um im Texteingabefeld den gelesenen Dateiinhalt anzuzeigen. Abschließend wird eine Erfolgsmeldung in die Fensteroberfläche ausgegeben.

Wenn Sie das Beispielprogramm starten und ausprobieren, stellen Sie fest, dass es wie gewünscht funktioniert. Dennoch ist es verbesserungsfähig. Die Leseroutine, um Zeichen aus der Datei in das Programm zu kopieren, arbeitet nämlich äußerst ineffizient. Es wird momentan immer nur ein einziges Zeichen mit read() gelesen. Jeder Zugriff auf externe Geräte, in diesem Fall die Festplatte, ist jedoch aufwändig, so dass dringend empfohlen wird, Zugriffe zu minimieren. Eine erste verbesserte Leseroutine wird Ihnen im Folgenden vorgestellt.

import java.awt.*; 
import java.awt.event.*; 
import java.io.*; 

public class MyApplication extends Frame implements ActionListener, WindowListener 
{ 
  static MyApplication myapp; 
  static Label label; 
  static TextArea text; 
  static Button save, load; 

  public static void main(String[] args) 
  { 
    myapp = new MyApplication(); 
    myapp.setLayout(new BorderLayout()); 
    myapp.setSize(500, 400); 
    label = new Label(); 
    myapp.add(label, BorderLayout.NORTH); 
    text = new TextArea(); 
    myapp.add(text, BorderLayout.CENTER); 
    Panel panel = new Panel(new GridLayout(1, 2)); 
    myapp.add(panel, BorderLayout.SOUTH); 
    save = new Button("Text speichern"); 
    save.addActionListener(myapp); 
    panel.add(save); 
    load = new Button("Text laden"); 
    load.addActionListener(myapp); 
    panel.add(load); 
    myapp.addWindowListener(myapp); 
    myapp.setVisible(true); 
  } 

  public void actionPerformed(ActionEvent ev) 
  { 
    try 
    { 
      if (ev.getSource() == save) 
      { 
        FileWriter file = new FileWriter("brief.txt"); 
        String message = text.getText(); 
        file.write(message, 0, message.length()); 
        file.close(); 
        label.setText("Datei wurde erfolgreich gespeichert."); 
      } 
      else 
      { 
        FileReader file = new FileReader("brief.txt"); 
        StringBuffer message = new StringBuffer(); 
        char c[] = new char[100]; 
        int i; 
        while (file.ready()) 
        { 
          i = file.read(c, 0, c.length); 
          message.append(c, 0, i); 
        } 
        file.close(); 
        text.setText(message.toString()); 
        label.setText("Datei wurde erfolgreich geladen."); 
      } 
    } 
    catch (IOException ex) 
    { 
      label.setText("Fehler: " + ex.getMessage()); 
    } 
  } 

  public void windowClosing(WindowEvent ev) 
  { 
    myapp.setVisible(false); 
    myapp.dispose(); 
    System.exit(1); 
  } 

  public void windowActivated(WindowEvent ev) { } 
  public void windowClosed(WindowEvent ev) { } 
  public void windowDeactivated(WindowEvent ev) { } 
  public void windowDeiconified(WindowEvent ev) { } 
  public void windowIconified(WindowEvent ev) { } 
  public void windowOpened(WindowEvent ev) { } 
} 

Ab sofort wird bei einem Lesezugriff nicht mehr nur genau ein einziges Zeichen aus der Datei in das Programm übertragen, sondern nun wird in jedem Schleifendurchgang versucht, bis zu 100 Zeichen gleichzeitig zu lesen. Dazu wird ein Array bestehend aus 100 Zeichen definiert. Dieses Array wird als Parameter an eine Methode read() übergeben. Außerdem bekommt die Methode im zweiten und dritten Parameter angegeben, ab welcher Position die gelesenen Zeichen im Array abgelegt werden sollen und wie viele Zeichen ab der Startposition im Array gespeichert werden sollen. Im Beispiel ist als Startposition 0 angegeben, so dass das Array ab der ersten Stelle mit gelesenen Zeichen gefüllt wird. Die maximale Anzahl im Array zu speichernder Zeichen ist identisch mit der Länge des Arrays, also 100.

Diese Methode read() gibt als Ergebnis einen Wert vom Typ int zurück, der angibt, wie viele Zeichen tatsächlich gelesen und in das Array übertragen wurden. Das heißt, es kann sein, dass read() weniger Zeichen ins Array überträgt als durch den dritten Parameter in read() angegeben sind - das ist kein Problem und völlig okay. Wenn Ihre Datei zum Beispiel nur aus 10 Zeichen besteht, so werden durch read() natürlich auch nur diese 10 Zeichen gelesen und im Array abgelegt. Es sind ja gar keine 100 Zeichen vorhanden, die theoretisch auf einmal gelesen und im Array abgelegt werden könnten.

Wenn die gelesenen Werte im Array an das Objekt vom Typ java.lang.StringBuffer angehängt werden sollen, werden nun der Methode append() zwei zusätzliche Parameter übergeben, die beide vom Typ int sind. Diese Parameter haben eine ähnliche Funktion wie die beiden int-Werte der eben aufgerufenen Methode read(). Sie geben an, ab welcher Stelle im Array Zeichen übertragen und an das Objekt vom Typ java.lang.StringBuffer angehängt werden sollen und wie viele. Die Startposition ist 0, die Anzahl der zu übertragenden Zeichen hängt davon ab, wie viele mit read() gelesen und im Array abgelegt wurden. Daher wird der Rückgabewert von read(), der die Anzahl der gelesenen Zeichen enthält, als dritter Parameter an die Methode append() übergeben.

Wenn Sie das so modifizierte Beispielprogramm ausführen funktioniert es immer noch wie vorher. Dennoch arbeitet das Programm nun effizienter, da Zeichen nicht mehr einzeln, sondern jeweils im Block mit bis zu 100 Zeichen gelesen werden. Dadurch werden die direkten Lesezugriffe auf die Datei verringert, was das Programm erheblich effizienter arbeiten lässt.

Es gibt eine andere Möglichkeit, das Programm zu optimieren und die Leseroutine effizienter zu gestalten. Dazu wird auf den Stream java.io.BufferedReader zugegriffen, der eine Pufferung anbietet. Dieser Stream wird im folgenden Beispielprogramm verwendet und mit dem Stream vom Typ java.io.FileReader verkettet.

import java.awt.*; 
import java.awt.event.*; 
import java.io.*; 

public class MyApplication extends Frame implements ActionListener, WindowListener 
{ 
  static MyApplication myapp; 
  static Label label; 
  static TextArea text; 
  static Button save, load; 

  public static void main(String[] args) 
  { 
    myapp = new MyApplication(); 
    myapp.setLayout(new BorderLayout()); 
    myapp.setSize(500, 400); 
    label = new Label(); 
    myapp.add(label, BorderLayout.NORTH); 
    text = new TextArea(); 
    myapp.add(text, BorderLayout.CENTER); 
    Panel panel = new Panel(new GridLayout(1, 2)); 
    myapp.add(panel, BorderLayout.SOUTH); 
    save = new Button("Text speichern"); 
    save.addActionListener(myapp); 
    panel.add(save); 
    load = new Button("Text laden"); 
    load.addActionListener(myapp); 
    panel.add(load); 
    myapp.addWindowListener(myapp); 
    myapp.setVisible(true); 
  } 

  public void actionPerformed(ActionEvent ev) 
  { 
    try 
    { 
      if (ev.getSource() == save) 
      { 
        FileWriter file = new FileWriter("brief.txt"); 
        String message = text.getText(); 
        file.write(message, 0, message.length()); 
        file.close(); 
        label.setText("Datei wurde erfolgreich gespeichert."); 
      } 
      else 
      { 
        BufferedReader file = new BufferedReader(new FileReader("brief.txt")); 
        StringBuffer message = new StringBuffer(); 
        char c; 
        while (file.ready()) 
        { 
          c = (char)file.read(); 
          message.append(c); 
        } 
        file.close(); 
        text.setText(message.toString()); 
        label.setText("Datei wurde erfolgreich geladen."); 
      } 
    } 
    catch (IOException ex) 
    { 
      label.setText("Fehler: " + ex.getMessage()); 
    } 
  } 

  public void windowClosing(WindowEvent ev) 
  { 
    myapp.setVisible(false); 
    myapp.dispose(); 
    System.exit(1); 
  } 

  public void windowActivated(WindowEvent ev) { } 
  public void windowClosed(WindowEvent ev) { } 
  public void windowDeactivated(WindowEvent ev) { } 
  public void windowDeiconified(WindowEvent ev) { } 
  public void windowIconified(WindowEvent ev) { } 
  public void windowOpened(WindowEvent ev) { } 
} 

Wenn Sie die Klasse java.io.BufferedReader einsetzen und über den Stream java.io.FileReader legen, werden Lesevorgänge durch einen Puffer optimiert. Auch wenn also im obigen Beispiel die Leseroutine in der while-Schleife wieder in ihre ursprüngliche Form gebracht wurde und immer nur ein Zeichen liest, so werden die Zeichen jedoch nur einzeln aus dem Puffer übertragen, der jedoch seinerseits Zeichen im Block und nicht einzeln aus der Datei liest.

Genaugenommen funktioniert die Klasse java.io.BufferedReader also genauso wie Ihr Code im vorherigen Beispiel: Es werden jeweils mehrere Zeichen auf einmal aus der Datei gelesen und innerhalb eines Puffers abgelegt. Dieser Puffer könnte zum Beispiel in Form eines Arrays vom Typ char implementiert sein. Wenn mit read() auf den Puffer zugegriffen wird, werden die Zeichen einzeln aus dem Puffer in die Variable c übertragen. Solange der Puffer Zeichen enthält, ist kein weiterer Zugriff auf die Datei nötig. Dieses blockweise Lesen von Zeichen aus der Datei erfolgt also nun mit Hilfe der Klasse java.io.BufferedReader genauso wie im vorherigen Beispielprogramm, bei dem Sie selber das blockweise Lesen mit Hilfe eines Arrays programmieren mussten.

Da der Zugriff auf externe Geräte wie zum Beispiel auf die Festplatte aufwändig ist, sollten Sie grundsätzlich immer einen Puffer verwenden. Für zeichenorientiertes Lesen steht die Klasse java.io.BufferedReader, für byteorientiere Lesevorgänge die Klasse java.io.BufferedInputStream zur Verfügung.

Auch für Schreibvorgänge wie das Speichern von Daten in einer Datei existieren Puffer. Die Standardbibliothek bietet die Klassen java.io.BufferedWriter und java.io.BufferedOutputStream an, um Daten gepuffert zu schreiben. Grundsätzlich ist es vorzuziehen, auch bei Schreibvorgängen auf Puffer zuzugreifen. Dennoch kann es zum Beispiel aus Sicherheitsgründen notwendig sein, dass Daten, die in eine Datei geschrieben werden, auch sofort dort gespeichert und nicht erst in einem Puffer zurückgehalten werden. Würde das Programm abstürzen und wären die Daten zu diesem Zeitpunkt noch nicht aus dem Puffer in die Datei übertragen worden, wären die Daten verloren und nicht gespeichert.


4.7 Arrays lesen und schreiben

Arrays als Datenquelle und Datensenke verwenden

Im folgenden Beispiel wird Ihnen gezeigt, wie Sie den Stream java.io.ByteArrayInputStream verwenden, um Daten aus einem Array zu lesen. In diesem Fall wird also ein Array innerhalb des Programms als Datenquelle betrachtet.

import java.applet.*; 
import java.awt.*; 
import java.awt.event.*; 
import java.io.*; 

public class MyApplet extends Applet implements ActionListener 
{ 
  byte[] numbers; 
  Label label; 
  List list; 
  Button button; 

  public void init() 
  { 
    setLayout(new BorderLayout()); 
    numbers = new byte[10]; 
    for (int i = 0; i < numbers.length; ++i) 
    { 
      numbers[i] = (byte)(Math.random() * 100); 
    } 
    label = new Label(); 
    add(label, BorderLayout.NORTH); 
    list = new List(); 
    add(list, BorderLayout.CENTER); 
    button = new Button("Liste laden"); 
    button.addActionListener(this); 
    add(button, BorderLayout.SOUTH); 
  } 

  public void actionPerformed(ActionEvent ev) 
  { 
    try 
    { 
      ByteArrayInputStream bytes = new ByteArrayInputStream(numbers); 
      int i; 
      while ( (i = bytes.read()) != -1) 
      { 
        list.add(Integer.toString(i)); 
      } 
      bytes.close(); 
      label.setText("Das Array wurde erfolgreich eingelesen."); 
    } 
    catch (IOException ex) 
    { 
      label.setText("Fehler: " + ex.getMessage()); 
    } 
  } 
} 

Das Applet erstellt innerhalb der Methode init() eine Benutzeroberfläche bestehend aus einer Textbox zum Anzeigen von Erfolgs- und Fehlermeldungen, einer Liste und einer Schaltfläche, die bei Betätigen aus einem Array Zahlen liest und diese in der Liste anzeigt. Der Lesezugriff aufs Array erfolgt in diesem Fall mit Hilfe der Klasse java.io.ByteArrayInputStream, um den Einsatz dieses byteorientierten Streams zu verdeutlichen. Selbstverständlich könnte dieses Beispiel auch anderweitig ohne Streams programmiert werden.

Innerhalb von init() wird nicht nur die Benutzerschnittstelle erstellt, sondern außerdem ein Array vom Typ byte angelegt und mit Zufallszahlen gefüllt. Dies geschieht in einer for-Schleife, die in jedem Schleifendurchgang auf die Klasse java.lang.Math zugreift und die Methode random() aufruft. Diese Methode gibt eine Zufallszahl vom Typ double zwischen 0,0 und 1,0 zurück. Der Rückgabewert von random() wird im Beispiel mit 100 multipliziert. Auf diese Weise werden Zufallszahlen zwischen 0 und 100 erhalten.

Bei Betätigen der Schaltfläche, um die Liste zu laden, wird ein Stream geöffnet. Es wird ein Objekt vom Typ java.io.ByteArrayInputStream angelegt. Dem Konstruktor muss ein Array vom Typ byte übergeben werden, das als Datenquelle vom Stream verwendet werden soll. In diesem Fall wird das Array als Datenquelle angegeben, das in init() mit Zufallszahlen gefüllt wurde. Wie bei Streams üblich wird auch hier die gesamte Leseroutine in eine try-catch-Klausel gestellt, um im Falle eines Fehlers eine Ausnahme vom Typ java.io.IOException abzufangen.

Der Lesevorgang gestaltet sich nur minimal anders als bei den im Abschnitt 4.6, „Dateien lesen und schreiben“ vorgestellten zeichenorientierten Streams. Byteorientierte Streams bieten keine Methode ready() an, um herauszufinden, ob in einem Stream neue Daten bereit liegen und gelesen werden können.

Die Lösung im obigen Beispiel bedient sich lediglich der Methode read(), um Daten aus dem Stream zu lesen. Diese Methode gibt jeweils das nächste zu lesende Byte zurück oder aber den Wert -1, wenn das Ende des Streams erreicht wurde. Innerhalb der while-Schleife wird der Rückgabewert von read() in einer Variablen i vom Typ int gespeichert und daraufhin verglichen, ob er ungleich -1 ist. Ist dies der Fall, handelt es sich um ein gültiges gelesenes Byte, das nun in der Variablen i vorliegt und innerhalb der Schleife der Liste hinzugefügt wird. Gibt read() jedoch den Wert -1 zurück, so wird die Schleife beendet und der Stream mit close() geschlossen.

Wie bei Streams üblich wissen Sie nie, wie viele Daten vom Stream gelesen werden können. Lesende Zugriffe finden also im Allgemeinen immer in einer Schleife statt, die neue Daten zu lesen versucht, bis sie am Ende des Streams ankommt.

Im folgenden Beispielprogramm wird Ihnen gezeigt, wie ein Array vom Typ byte als Datensenke verwendet werden kann, um über einen Stream dort Bytes abzulegen. Im Applet wird hierzu eine Texteingabebox erstellt, in die Zahlen getrennt durch Leerzeichen oder Komma eingegeben werden können. Wenn auf eine Schaltfläche geklickt wird, werden diese Zahlen über einen Stream in ein Array vom Typ byte ausgegeben. Damit Sie die Funktionsweise des Applets kontrollieren können, können die im Array gespeicherten Zahlen in eine Liste ausgegeben werden. Der entsprechende Code zum Lesen der Bytes aus dem Array und zur Ausgabe in die Liste ist aus dem vorherigen Beispiel übernommen worden.

import java.applet.*; 
import java.awt.*; 
import java.awt.event.*; 
import java.io.*; 
import java.util.*; 

public class MyApplet extends Applet implements ActionListener 
{ 
  byte[] numbers = null; 
  TextField input; 
  List list; 
  Button save, load; 

  public void init() 
  { 
    setLayout(new BorderLayout()); 
    input = new TextField(); 
    add(input, BorderLayout.NORTH); 
    list = new List(); 
    add(list, BorderLayout.CENTER); 
    Panel panel = new Panel(new GridLayout(1, 2)); 
    save = new Button("Zahlen speichern"); 
    save.addActionListener(this); 
    panel.add(save); 
    load = new Button("Liste laden"); 
    load.addActionListener(this); 
    panel.add(load); 
    add(panel, BorderLayout.SOUTH); 
  } 

  public void actionPerformed(ActionEvent ev) 
  { 
    try 
    { 
      if (ev.getSource() == save) 
      { 
        ByteArrayOutputStream bytes = new ByteArrayOutputStream(); 
        StringTokenizer tokenizer = new StringTokenizer(input.getText(), " ,"); 
        while (tokenizer.hasMoreTokens()) 
        { 
          bytes.write(Byte.parseByte(tokenizer.nextToken())); 
        } 
        numbers = bytes.toByteArray(); 
        bytes.close(); 
      } 
      else if (numbers != null) 
      { 
        ByteArrayInputStream bytes = new ByteArrayInputStream(numbers); 
        int i; 
        while ( (i = bytes.read()) != -1) 
        { 
          list.add(Integer.toString(i)); 
        } 
        bytes.close(); 
      } 
    } 
    catch (IOException ex) 
    { 
      System.err.println("IOException: " + ex.getMessage()); 
    } 
    catch (NumberFormatException ex) 
    { 
      System.err.println("NumberFormatException: " + ex.getMessage()); 
    } 
  } 
} 

Der Code, der den Schreibzugriff auf den Stream java.io.ByteArrayOutputStream ausführt, steht innerhalb der Methode actionPerformed() hinter der if-Überprüfung. Es wird zuerst der Stream erstellt, indem ein Objekt vom Typ java.io.ByteArrayOutputStream angelegt wird. Über die Referenzvariable bytes wird in der folgenden while-Schleife wiederholt auf den Stream zugegriffen und über eine Methode write() jeweils ein Byte in den Stream hineingeschrieben. Ist der Schreibvorgang beendet und wird die while-Schleife verlassen, wird über den Aufruf der Methode toByteArray() ein Array vom Typ byte erhalten, in dem sich alle Bytes befinden, die in den Stream ausgegeben worden waren. Abschließend wird der Stream mit close() geschlossen, da er nicht weiter benötigt wird.

Die Klasse java.util.StringTokenizer wird verwendet, um die Zeichenkette, die der Anwender in die Texteingabebox eingegeben hat, zu sezieren. Indem ein Objekt vom Typ java.util.StringTokenizer erstellt wird und dem Konstruktor als erster Parameter die zu bearbeitende Zeichenkette und als zweiter Parameter die möglichen Trennzeichen in Form eines Strings übergeben werden, kann über die Methode nextToken() jeweils auf den nächsten Zeichenkettenabschnitt zugegriffen werden, solange hasMoreTokens() den Rückgabewert true liefert. Da als Trennzeichen vom Anwender Leerzeichen und Komma verwendet werden können, um mehrere Zahlen anzugeben, wird als zweiter Parameter an den Konstruktor auch ein String bestehend aus einem Leerzeichen und einem Komma übergeben. Jeder Aufruf von nextToken() liefert nun eine neue Zahl zurück. Weil diese Methode java.lang.String als Datentyp des Rückgabewertes verwendet, wird mit Hilfe der Klasse java.lang.Byte und deren Methode parseByte() der Rückgabewert in ein Byte umgewandelt wird. Dies ist notwendig, weil in den Stream java.io.ByteArrayOutputStream lediglich Bytes geschrieben werden können - die Methode write() erwartet Parameter vom Typ byte.

Beachten Sie, dass die Methode parseByte() fehlschlägt, wenn Sie keine Zahlen eingeben, sondern zum Beispiel Buchstaben. In diesem Fall wird eine Ausnahme vom Typ java.lang.NumberFormatException geworfen. Das ist der Grund, warum ein zweiter catch-Block in den Code eingefügt wurde. Nun wird nicht mehr nur eine Ausnahme vom Typ java.io.IOException abgefangen, sondern zusätzlich auch eine vom Typ java.lang.NumberFormatException. Natürlich wäre es ohne Probleme möglich, die beiden catch-Blöcke zusammenzuführen und lediglich eine Ausnahme vom Typ java.lang.Exception abzufangen. Sowohl java.io.IOException als auch java.lang.NumberFormatException sind Kindklassen von java.lang.Exception.

Abschließend noch eine kurze Erläuterung des Codes in den catch-Anweisungsblöcken: Es wird auf ein Objekt err zugegriffen, das die Standardfehlerausgabe darstellt und als öffentliche Eigenschaft der Klasse java.lang.System jederzeit zur Verfügung steht. Das Objekt err basiert auf der Klasse java.io.PrintStream - es handelt sich also um einen Stream. Über diesen zeichenorientierten Stream können jederzeit Daten ausgegeben werden. Sie brauchen diesen Stream auch nicht erstellen, öffnen oder schließen - er ist einfach immer automatisch da. Sie verwenden err als primitiven Datenausgabemechanismus, um Fehlermeldungen anzuzeigen. Diese Fehlermeldungen erscheinen dann nicht innerhalb einer Fensteranwendung, sondern werden für gewöhnlich in einer Textkonsole ausgegeben.

Da err auf dem Stream java.io.PrintStream basiert, ist eine Datenausgabe sehr einfach möglich. Diese Klasse bietet nämlich zahlreiche Methoden an, um Informationen fast beliebiger Datentypen rasch ausgeben zu können. So ist zum Beispiel die Methode println() vielfach überladen, so dass über diese Methode zeilenweise Wahrheitswerte, Bytes, Zahlen vom Typ int und long, Zeichenketten in Form von java.lang.String und eines Arrays vom Typ char und so weiter ausgegeben werden können. Im Beispielprogramm wird genau diese Methode verwendet, um den Grund der Ausnahme auf die Standardfehlerausgabe auszugeben.


4.8 Aufgaben

Übung macht den Meister

Sie können die Lösungen zu allen Aufgaben in diesem Buch als ZIP-Datei erwerben.

  1. Erweitern Sie das unter Abschnitt 4.6, „Dateien lesen und schreiben“ als letztes Beispiel vorgestellte Programm dahingehend, dass auch der Speichervorgang, um Text in einer Datei zu sichern, mit Hilfe eines Puffers effizienter erfolgt.

    Sie müssen lediglich den Stream java.io.FileWriter im Stream java.io.BufferedWriter kapseln.

  2. Erweitern Sie Ihre Java-Application aus Aufgabe 1 dahingehend, dass beim Lesen einer Datei mitgezählt wird, aus wie vielen Zeilen die Datei besteht. Geben Sie nach einem erfolgreichen Lesevorgang innerhalb der Fensteroberfläche die Anzahl der gelesenen Zeilen aus.

    Greifen Sie auf den Stream java.io.LineNumberReader zu. Dieser bietet unter anderem eine Methode getLineNumber() an. Rufen Sie diese Methode für den Stream auf, nachdem die gesamte Datei gelesen und das Ende des Streams erreicht wurde.

  3. Erweitern Sie Ihre Java-Application aus Aufgabe 2 dahingehend, dass nicht mehr automatisch eine im Code fest vorgegebene Datei gelesen wird, sondern der Anwender über ein Dialogfenster selber auswählen kann, welche Datei auf seiner Festplatte geladen werden soll.

    Was sich auf den ersten Blick möglicherweise schwer zu entwickeln anhört, ist tatsächlich sehr einfach zu implementieren. Sie benötigen lediglich die Klasse java.awt.FileDialog, mit deren Hilfe Dialogfenster zum Laden und Speichern von Dateien eingeblendet werden können.