PI4 Logo

I/O-Operationen in Java

Exceptions

In fast allen Programmen kann es an diversen Stellen zu Situationen kommen, die den normalen Programmablauf unterbrechen. Solche "Ausnahmesituationen" treten beispielsweise auf, wenn nicht genügend Speicher bereitgestellt werden kann, oder wenn eine Datei nicht geöffnet werden kann. Es sind also Probleme, die erst zur Laufzeit des Programmes auftreten.

Mit dem von Java unterstützten Exception-Handling kann man auf Fehler in sehr komplizierten Situationen einfach und auf sehr differenzierte Art und Weise reagieren. Der eigentliche Programmcode wird von den vielen einzelnen Fehlerabfragen gesäubert.

Tritt innerhalb einer Methode eine solche Ausnahmesituation auf, kann der Programmierer an dieser Stelle eine Exception "auswerfen" (Viele interne Methoden von Java nutzen diesen Mechanismus in Fehlerfällen). Die Folge des Auswerfens einer Exception ist, daß der Programmablauf an dieser Stelle unterbrochen wird und an einer Abfangstelle innerhalb des Programmes fortgesetzt wird. Die Abfangstelle muß vom Programmierer vorher festgelegt werden. Dies geschieht folgendermaßen: Wird innerhalb eines Programmes eine Methode aufgerufen, die eine Exception auswerfen kann, muß diese Methode in einer Abfangumgebung eingeschlossen werden.

Java´s Syntax zum Exception Handling

Um eine Exception auszuwerfen, kann die throw-Anweisung benutzt werden. Der throw-Anweisung muß ein Exception-Objekt folgen. Es gibt eine Reihe von vordefinierten Exception-Klassen, man kann aber auch eigene Exceptions definieren. Diese müssen von der Klasse Throwable abgeleitet werden. Methoden, die eine Excption auswerfen müssen dies bei der Deklaration der Methode im Methodenkopf mit angeben. Dies geschieht durch das Schlüsselwort throws gefolgt vom Typ der ausgeworfenen Exception. Es folgt ein Beispiel:


class MyException extends Exception
{
  string message;
  public MyException(string _message)
  {
     message= _message;
  }
  public String toString()
  {
     return ("MyException: " + message");
  }
}

void mymethod() throws MyException
{
  ...
  if ("something went wrong")
     throw new MyException("something went wrong");
  ...
}

Um die Methode mymethod benutzen zu können, muß ihr Aufruf in eine try-catch Umgebung eingeschlossen werden:

  ...
  try {
    mymethod();
  }
  catch (MyException e)
  {
    System.out.println(e);
  }

Praktisch alle IO-Operationen von Java werfen eine IOException, so daß Blöcke mit Ein-/Ausgabe-Aufrufen immer in ein entsprechendes try/catch eingeschlossen werden müssen:

import java.io.*;

public class IOBeispiel
{
  public static void main(String[] args)
    {
      System.out.println("IO-Beispiel\n");


      String zeile= "";
      int    wert;
     
      BufferedReader in= new BufferedReader(new InputStreamReader(System.in) );

      try {
        zeile= in.readLine();
      } catch (IOException ioe) {}

      wert= Integer.parseInt(zeile);

      System.out.println("wert= " + wert);
    };
};

Streams

Definition des Begriffes "Stream"

In Java werden für die Ein- und Ausgabe sogenannte Streams benutzt. Streams dienen zur Umwandlung von Objekten beliebiger Typen in einen Strom aus ASCII-Zeichen (oder Bytes) und umgekehrt. Die Hauptanwendung liegt dementsprechend natürlich bei der zeichenorientierten (oder auch binären) Ein- und Ausgabe; sie stellen also die Schnittstelle des Programms zum Dateisystem des Rechners dar.

Beispielsweise dient ein Stream also auch dazu, eine Zahl inform von ASCII-Zeichen in eine Datei zu schreiben, bzw. aus einer Datei auszulesen. In Java (wie auch in vielen anderen Programmiersprachen) werden sogenannte Standardeingabe- und Standardausgabe-Streams definiert:

Standardeingabe-Stream:
Werden Daten auf den Standardausgabe-Stream geschrieben, hat dies zur Folge, daß diese i.d.R. auf dem Bildchirm ausgegeben werden. Der Standardausgabe-Stream ist in der Klasse System definiert und heißt dementsprechen System.out und ist vom Typ FileOutputStream.
Standardausgabe-Streams:
Werden Daten vom Standardeingabe-Stream gelesen, hat dies zur Folge, daß diese i.d.R. per Tastatur eingegeben werden können. Der Standardeingabe-Stream ist ebenfalls in der Klasse System definiert und heißt dementsprechen System.in und ist vom Typ FileInputStream.

Java´s Stream-Klassen

Die wesentlichen beteiligten Klassen für Streams und deren Exceptions finden sich im Package java.io. Es gibt es zwei abstrakte Basisklassen InputStream und OutputStream. Diese definieren die Mindestfunktionalität eines Eingabe- bzw. Ausgabestromes. Mit anderen Worten: sie deklarieren eine Reihe von Methoden (wie z.B. read() - zum Einlesen eines einzelnen Zeichens ), die von spezialisierten Klassen definiert und um weitere Funktionen erweitert werden. Die wichtigsten spezialisierten Klassen sind:

ByteArrayInputStream und ByteArrayOutputStream:
Streams zur Ein-/Ausgabe von "rohen" Daten in Form von Bytefolgen.
FileInputStream und FileOutputStream:
Verbindung zwischen Streams und dem lokalen Filesystem.
PipedInputStream und PipedOutputStream:
zwei Enden einer Pipeline, werden immer paarweise verwendet.
DataInputStream und DataOutputStream:
zusätzliche Ein-/Ausgabemöglichkeiten für eingebaute Typen und Strings.
BufferedInputStream und BufferedOutputStream:
implementieren eine Zwischenpufferung ein-/auszugebender Daten.
PrintStream:
Strom, der für die formatierte Ausgabe eingebauter Typen (wie z.B. int, double usw.) gedacht ist.

Einlesen von Zeichen aus Dateien

Die oben genannten Klassen dienen zum (binären) Einlesen bzw. Ausgeben, d.h. deren Methoden operieren mit Bytes. Glücklicherweise gibt es für die meisten der angegebenen Stream-Klassen aber ab Java 1.2 Versionen namens "-Reader" bzw. "-Writer". Sie sind auf die Ein-/Ausgabe von Zeichen (statt Bytes) spezialisiert. Solche Reader bilden gewissermaßen eine Zwischenschicht zwischen den Stream-Klassen und der Anwendung. Bei der Initialisierung muß ein Stream-Objekt übergeben werden aus dem gelesen bzw. in das geschrieben werden kann. Der Reader bildet dann die "binären" Daten in Unicode-Zeichen ab.

Für zeichenorientierte Dateien sollte man deshalb vorsichtshalber immer Reader und Writer verwenden, und normale Streams nur für binäre Ein- und Ausgaben.

Dateneingabe von der Tastatur

Zum Lesen von Zeichen aus Dateien, benutzt man am besten die Klasse InputStreamReader oder BufferedInputStreamReader. Der nachfolgende Code-Auszug zeigt, wie man zeichenweise aus der Standardeingabe ausliest:

  InputStreamReader isr=new InputStreamReader(System.in);
  int i;

  while ((i=isr.read())>=0) {
    System.out.println("gelesenes Zeichen: "+(char)i);
  }

In diesem Code-Auszug wird zunächst ein InputStreamReader erzeugt, und mit der Standard-Eingabe verbunden (1. Code-Zeile). Anschließend werden Daten aus der Standardeingabe Zeichenweise gelesen (While-Schleife), bis keine Daten mehr in der Standardeingabe vorhanden sind.

Zur Lösung des Übungsblattes Nr. 4 (und vieler weiterer Übungsblätter) ist es notwendig, Daten von der Tastatur entgegenzunehmen bzw. aus einer Datei auszulesen.

Diese Operationen unterscheiden sich vom Auslesen der Kommandozeilenparameter! Kommandozeilenparameter werden beim Programmstart angegeben, Eingabeoperationen verlaufen interaktiv im Verlauf des Programmablaufes.

Leider gibt es keine Möglichkeit, aus ASCII-Files beispielsweise einzelne int- oder double-Zahlen zu lesen. Dazu muß man eine Zeile als String einlesen, selbst in Teile zerlegen (z.B. mit einem StringTokenizer) und selbst die Umwandlung in den gewünschten Typ vornehmen. Dabei ist entweder die Klasse NumberFormat oder die jeweilige Wrapper-Klasse hilfreich:

      String s;
      // ... Einlesen aus der Datei nach s
      double d=Double.valueOf(s).doubleValue();

Beispiel

Das nachfolgende Java-Programm liest eine Integer-Zahl ein und gibt diese anschließend wieder aus:

import java.io.*;

public class IOBeispiel
{
  public static void main(String[] args)
    {
      System.out.println("IO-Beispiel\n");


      String zeile= "";
      int    wert;
     
      BufferedReader in= new BufferedReader(new InputStreamReader(System.in) );

      try {
        zeile= in.readLine();
      } catch (IOException ioe) {}

      wert= Integer.parseInt(zeile);

      System.out.println("wert= " + wert);
    };
};

Wird das Programm gestartet, so wird der String "IO-Beispiel" auf dem Bildschirm ausgegeben. Anschließend wartet das Programm auf eine Eingabe des Benutzers, der nun kann eine Zahl eingegeben kann (Abschluß der Eingabe mit der Return-Taste). Die Eingabe wird zunächst in der String-Variablen zeile abgelegt und anschließend mit Hilfe von Integer.parseInt(zeile) wieder ausgegeben. Falls die Eingabe kein Integerwerten ist, wird das Programm mit einem Fehler (Exception) abgebrochen.

Beispielaufruf (unterstrichener Text spiegelt Tastatureingaben wieder):

 > java IOBeispiel
IO-Beispiel
1234
wert= 1234


Es sei noch einmal darauf hingewiesen, daß es zum Lösen der Übungsaufgaben nicht notwendig ist, genau zu verstehen, was sich hinter einem BufferedReader-Objekt verbirgt! Das Beispiel sollte jedoch vergleichsweise leicht zum Lösen der Übungsaufgaben angepaßt werden können.

Zusatzinformationen

Hier noch ein Auszug aus der API-Dokumentation von Java-Soft:

readLine

public String readLine() throws IOException
Read a line of text. A line is considered to be terminated by any one of a line feed ('\n'), a carriage return ('\r'), or a carriage return followed immediately by a linefeed.
Returns:
A String containing the contents of the line, not including any line-termination characters, or null if the end of the stream has been reached
Throws:
IOException - If an I/O error occurs

Dateneingabe aus einer Datei

Die Eingabe von Daten aus einer Datei erfolgt in ähnlicher Weise wie die Eingabe von der Tastatur. Es muß lediglich zuerst die Datei geöffnet werden, aus der die Daten ausgelesen werden sollen. Dies geschieht, indem man ein FileReader-Objekt erzeugt. Bei der Initialisierung dieses Objektes muß der Name der Datei angegeben werden, die geöffnet werden soll.

Beispiel

Das nachfolgende Beispiel liest 64 Integerwerte aus einer Datei aus, deren Name per Kommandozeile übergeben wird. Die eingelesenen Werte werden sofort wieder ausgegeben.

import java.io.*;
import java.util.StringTokenizer;

public class IOBeispiel
{
  public static void main(String[] args)
    {
      String zeile= "";
      int    wert;

      FileReader fr= null;
      BufferedReader in= null;

      try {
        fr= new FileReader(args[0]);      
        in= new BufferedReader(fr);
      } catch(FileNotFoundException fnfe) {}

      for (int i= 0; i < 8; i++)
        {
          try {
            zeile= in.readLine();
          } catch (IOException ioe) {}

          StringTokenizer st= new StringTokenizer(zeile);
          for (int j= 0; j < 8; j++)
            {
              String s= st.nextToken();
              wert= Integer.parseInt(s);
              System.out.print(wert + "\t");
            }
          System.out.println("");
        }
    };
};

Beispielaufruf (unterstrichener Text spiegelt Tastatureingaben wieder):

 > java IOBeispiel test.txt
1       2       3       4       5       6       7       8
9       10      11      12      13      14      15      16
17      18      19      20      21      22      23      24
25      26      27      28      29      30      31      32
33      34      35      36      37      38      39      40
41      42      43      44      45      46      47      48
49      50      51      52      53      54      55      56
57      58      59      60      61      62      63      64


StringTokenizer

Ein StringTokenizer Objekt ermöglicht es, einen String in sogenannte Tokens zu zerlegen. Ein Token besteht aus einer beliebigen Folge von Buchstaben, Zahlen und Sonderzeichen. Die einzelnen Token sind durch sogenannte Delimeter getrennt. Default-Delimeter sind beispielsweise das Leerzeichen (´ ´), das Tabulator-Zeichen (´\t´) und der Zeilenumbruch (´\n´).

Es folgt ein Anwendungsbeispiel des String-Tokenizers:
  StringTokenizer st = new StringTokenizer("this is a test");
  while (st.hasMoreTokens()) {
    println(st.nextToken());
  }
Der obige Code-Auszug produziert folgende Ausgabe:
 this
 is
 a
 test
Weitere Informationen zum StringTokenizer finden sich in der API-Dokumentation von Java-Soft.
Christoph Kuhmünch <cjk@pi4.informatik.uni-mannheim.de>
Last modified: Thu Dec 2 11:32:17 MET 1999