Javaschubla.de - Java als erste Programmiersprache

Exceptions

"Exceptions" oder "Ausnahmen" sind Fehler. Sie treten zur Laufzeit auf, nicht beim Kompilieren. Auf ein paar Exceptions sind wir schon getroffen. Ein paar Beispiele:

    int[] intArray = new int[10];
    int[10] = 5; // wirft eine ArrayIndexOutOfBoundsException

    FileReader fr = new FileReader("test.txt"); // wirft eine FileNotFoundException,
    // welches eine Art von IOException ist, falls test.txt nicht existiert

    String s = new BufferedReader(new InputStreamReader(System.in)).readLine();
    Integer.parseInt(s); // wirft eine NumberFormatException, wenn s kein int ist

    String s = null;
    int länge = s.length(); // wirft eine NullPointerException

Das Programm stürzt dann an der Stelle ab und gibt eine Fehlermeldung mit Stacktrace aus. In der Stacktrace sieht man, in welcher Zeile der Fehler aufgetreten ist, d.h. wo die Exception "geworfen" wurde, und woher diese Methode aufgerufen wurde (und woher diese aufgerufen wurde und woher diese aufgerufen wurde usw. bis hoch zu main).

Man kann eine Exception auch "fangen". Dazu umgibt man die Zeilen, in denen ein Fehler auftreten kann, mit einem try-catch-Block. Die potentiell fehlerverursachenden Zeilen kommen in den try-Block. Die Fehlerbehandlung, z.B. Ausgabe der Fehlermeldung, kommt in den catch-Block. Das Programm läuft nach dem Fehler ganz normal weiter. em>Wichtig: Auch die Zeilen, die von der fehlerverursachenden Zeile abhängen, z.B. eine Variable lesen, die dort gesetzt werden soll und im Fehlerfall nun noch null ist, müssen mit in den try-Block, damit sie im Fehlerfall übersprungen werden.

Nehmen wir das Beispiel AlterEingabe aus der Lektion "Von der Tastatur lesen". Ignorier vorerst "throws IOEXception".

import java.io.*;

class AlterEingabeMitFehlerbehandlung
{
  public static void main(String[] args) throws IOException
  {
    InputStreamReader isr = new InputStreamReader(System.in);
    BufferedReader br = new BufferedReader(isr);
    System.out.print("Wie alt bist du: ");
    String eingabe = br.readLine();

    try
    {
      int alter = Integer.parseInt(eingabe);

      if (alter >= 18)
      {
        System.out.println("Hier ist dein bestellter Wodka.");
      }
      else
      {
        System.out.println("Kein Wodka für dich.");
      }
    }
    catch(NumberFormatException ex)
    {
      System.err.println("Das ist keine ganze Zahl!");
    }

    System.out.println("Programmende");
  }
}

Wenn eine ganze Zahl eingegeben wird, ist Integer.parseInt(eingabe) erfolgreich. Der folgende if-else-Block wird ausgeführt. Der catch-Block wird übersprungen, da ja keine NumberFormatException aufgetreten ist.

Wenn aber Buchstaben, eine Dezimalzahl (z.B. 17.5) oder aber auch eine zu große Zahl für int (z.B. 1234567890123) eingegeben wird, wird alles nach Integer.parseInt nicht mehr ausgeführt. Der Programmfluss springt zum catch-Block, die Fehlermeldung "Das ist keine ganze Zahl" wird ausgegeben. Das Programm stürzt nicht ab, es wird keine Stacktrace ausgegeben. Der Rest des Programms läuft normal weiter, d.h. egal ob eine NumberFormatException aufgetreten ist oder nicht, wird nun die Zeile System.out.println("Programmende"); ausgeführt.

Exceptions kann man nicht nur fangen, man kann sie auch selbst werfen. Z.B. wollen wir in diesem Fall nicht, dass der Benutzer negative Zahlen eingibt. Negative Zahlen sollen genau so behandelt werden wie Eingaben mit Buchstaben oder Dezimalzahlen. Eine Exception wirf man mit throw.

import java.io.*;

class AlterEingabeMitFehlerbehandlung
{
  public static void main(String[] args) throws IOException
  {
    InputStreamReader isr = new InputStreamReader(System.in);
    BufferedReader br = new BufferedReader(isr);
    System.out.print("Wie alt bist du: ");
    String eingabe = br.readLine();

    try
    {
      int alter = Integer.parseInt(eingabe);

      if (alter < 0) throw new NumberFormatException("negative Zahl");

      if (alter >= 18)
      {
        System.out.println("Hier ist dein bestellter Wodka.");
      }
      else
      {
        System.out.println("Kein Wodka für dich.");
      }
    }
    catch(NumberFormatException ex)
    {
      System.err.println("Das ist keine natürliche Zahl!");
    }

    System.out.println("Programmende");
  }
}

Mit new NumberFormatException erzeugen wir eine neue NumberFormatException (ein NFE-Objekt, eine Instanz der Klasse NFE). Ihrem Konstruktor übergeben wir den String "negative Zahl". Das ist die Message oder Fehlermeldung. Wir können sie mit ex.getMessage() abfragen.

Mit throw werfen wir diese Exception. Die restlichen Zeilen (mit if (alter >= 18) etc.) werden übersprungen. Dann wird die geworfene NFE mit catch(NumberFormatException ex) gefangen - genau so als wäre die NFE bei Integer.parseInt(eingabe) aufgetreten.

catch(NumberFormatException ex) bedeutet auch, dass ex eine Variable vom Typ NFE ist. Wir wollen sie gleich mal verwenden und die in der Klasse NFE definierte Methode getMessage() aufrufen.

import java.io.*;

class AlterEingabeMitFehlerbehandlung
{
  public static void main(String[] args) throws IOException
  {
    InputStreamReader isr = new InputStreamReader(System.in);
    BufferedReader br = new BufferedReader(isr);
    System.out.print("Wie alt bist du: ");
    String eingabe = br.readLine();

    try
    {
      int alter = Integer.parseInt(eingabe);

      if (alter < 0) throw new NumberFormatException("negative Zahl");

      if (alter >= 18)
      {
        System.out.println("Hier ist dein bestellter Wodka.");
      }
      else
      {
        System.out.println("Kein Wodka für dich.");
      }
    }
    catch(NumberFormatException ex)
    {
      System.err.println(ex.getMessage());
    }
    System.out.println("Programmende");

  }
}

Wenn man nun -1 eingibt, erscheint die Fehlermeldung "negative Zahl". Tritt der Fehler bei Integer.parseInt(eingabe) auf, z.B. wenn man "abc" eingibt, erscheint die Fehlermeldung "For input string: "abc"".

Wir können auch die Exception selbst ausgeben lassen, um ihren Typ zu erfahren, einfach mit System.err.println(ex); Dabei wird die Fehlermeldung (Message) mit ausgegeben, z.B.

java.lang.NumberFormatException: negative Zahl
oder
java.lang.NumberFormatException: For input string: "abc"

Man kann auch die Stacktrace ausgeben lassen mit

ex.printStackTrace();

Außer Exceptions zu fangen und zu werfen, kann man auch deklarieren, dass eine Methode eine Exception wirft. Das ist nützlich, wenn die Fehlerbehandlung nicht in der Methode selbst erfolgen soll, sondern in der aufrufenden Methode. Dazu benutzt man das Schlüsselwort throws. (Ignoriere weiterhin "IOException", aber es wird gleich erklärt.)

import java.io.*;

class AlterEingabeMitFehlerdeklarierung
{
  public static void main(String[] args) throws IOException
  {
    System.out.print("Wie alt bist du: ");

    try
    {
      int alter = readPositiveNumber();

      if (alter >= 18)
      {
        System.out.println("Hier ist dein bestellter Wodka.");
      }
      else
      {
        System.out.println("Kein Wodka für dich.");
      }
    }
    catch(NumberFormatException ex)
    {
      System.err.println(ex);
    }

    System.out.println("Programmende");
  }

  static int readPositiveNumber() throws NumberFormatException, IOException
  {
    InputStreamReader isr = new InputStreamReader(System.in);
    BufferedReader br = new BufferedReader(isr);
    String eingabe = br.readLine();
    int zahl = Integer.parseInt(eingabe);
    if (zahl < 0) throw new NumberFormatException("negative Zahl");
    return zahl;
  }
}

Wenn der Benutzer eine natürliche Zahl eingibt, ist Integer.parseInt(eingabe) erfolgreich und auch in der nächsten Zeile wird keine Exception geworfen. Es wird einfach ganz normal mit return zahl; das Ergebnis zurück gegeben.

Wenn der Benutzer Buchstaben oder eine Dezimalzahl eingibt, wirft Integer.parseInt(eingabe) eine NumberFormatException und wenn die Zahl negativ ist wird in der nächsten Zeile eine NFE geworfen. Der Rest der Methode wird nicht ausgeführt, sondern die Methode kehrt sofort zurück, wenn die Exception geworfen wird. In der aufrufenden Methode ist dann also die Zeile int alter = readPositiveNumber nicht erfolgreich, sondern hat eine NFE geworfen. Die nächsten Zeilen werden also nicht ausgeführt, sondern der Programmfluss springt zum catch-Block, die Exception wird samt ihrer Message mit System.err.println(ex); ausgegeben. Das Programm geht nach dem catch-Block weiter, es wird also noch System.out.println("Programmende"); ausgeführt.

Gecheckte und ungecheckte Exceptions

Wenn eine Exception "checked" ist, bedeutet das, dass man sie entweder fangen oder deklarieren muss. Zu den checked Exceptions gehört z.B. die IOException.

Wenn eine Exception "unchecked" ist, bedeutet das, man muss sie nicht fangen und nicht deklarieren. RuntimeException und alle ihre Untertypen sind unchecked. Dazu gehören z.B. NullPointerException, ArrayIndexOutOfBoundsException und NumberFormatException.

(Deshalb ist NumberFormatException eigentlich kein gutes Beispiel gewesen für throws, denn man hätte sie gar nicht deklarieren müssen und auch nicht in der aufrufenden Methode fangen.)

Checked Exceptions: IOException, throws-Deklarierung

Lass mal oben das "throws IOException" an der readPositiveNumber-Methode weg und versuche dann zu kompilieren. Was geschieht?

Für die Zeile, in der br.readLine() vorkommt, wird beim Kompilieren die Fehlermeldung "unreported exception java.io.IOException; must be caught or declared to be thrown" ausgegeben. Dem Compiler ist bekannt, dass die Methode readLine() in der Klasse BufferedReader eine IOException werfen kann (dort steht hinter dem Methodennamen auch "throws IOException"). Da es eine checked Exception ist, muss sie gefangen werden. Oder eben wieder deklariert werden, dass sie geworfen werden kann, und dann in der aufrufenden Methode gefangen. Oder wieder deklariert, was wir hier auch gemacht haben, in der main-Methode. Letztendlich haben wir sie also gar nicht gefangen, das sollte man normalerweise natürlich nicht tun - das Programm wird einfach wieder an der Stelle abstürzen, wo der Fehler auftritt.

In unserem Programm kann aber gar keine IOException auftreten. Wir lesen nämlich von System.in, der Tastatur. Das weiß der Compiler aber nicht. Wir hätten mit dem BufferedReader auch aus einer Datei lesen können. Diese könnte vor unserem Zugriff geschützt sein oder der Benutzer könnte sie hinterhältigerweise löschen, nachdem FileReader fr = new FileReader(dateiname) erfolgreich ausgeführt wurde. Es hätte auch sein können, dass wir mit dem BufferedReader über ein Netzwerk, z.B. aus dem Internet, Daten beziehen, und die Netzwerkverbindung plötzlich zusammenbricht. Prinzipiell kann also br.readLine() plötzlich eine IOException (Input-Output-Exception) verursachen, weil der darunterliegende Datenstrom unterbrochen wurde. Deshalb deklariert die BufferedReader.readLine()-Methode, dass sie eine IOException werfen könnte und deshalb müssen wir sie fangen oder deklarieren.

ArrayIndexOutOfBoundsExceptions hingegen sollten nie passieren, wenn man korrekt programmiert. Es wäre auch sehr hinderlich, wenn diese Exception checked wäre - man müsste ja jede Verwendung eines Arrays in einen try-catch-Block einbetten, in dem diese Exception gefangen wird.

Ähnlich ist es mit der NullPointerException, sie sollte nicht passieren. Man kann ja mit if (variable == null) abfragen, ob eine Variable null ist, und dann diesen Fall separat behandeln und keine Methoden auf sie aufrufen.

Warum NumberFormatExceptions nicht checked sind, weiß ich nicht genau. Vermutlich, weil man Zahlen in richtigen Programmen oft über ein GUI eingibt und die Überprüfung, ob es sich um eine zulässige Eingabe handelt, bereits dort erfolgt. Oder sie werden aus einer Datei oder Datenbank gelesen, bei der man auch von vornherein weiß, dass dort nur gültige Werte enthalten sind, weil man sie selbst hinein geschrieben hat. Dann wäre es unnötig, bei Integer.parseInt() eine NFE zu fangen.

Einem Anfänger erscheinen Exceptions oft als nervig, weil der Compiler einen zwingt, sie zu fangen oder zu deklarieren, aber langfristig verhindern sie Programmierfehler. Man kann eben beim Lesen aus einer Datei nicht vergessen, dass der Benutzer diese plötzlich löschen oder die CD, auf der sie sich befindet, plötzlich aus dem Laufwerk nehmen könnte. Der Compiler erinnert einen daran, dass BufferedReader.readLine() eine IOException werfen kann.

Exceptions niemals abwürgen

Niemals sollte man eine Exception einfach mit einem leeren catch-Block zum Schweigen bringen! Das führt zu fast unauffindbaren Programmierfehlern. Wenn man eine Exception mit einem leeren catch-Block fängt, läuft das Programm ja direkt nach diesem catch-Block weiter - ist aber normalerweise in einem inkonsistenten Zustand, da ja irgendetwas nicht erfolgreich verlaufen ist. Meistens tritt dann irgendwann später eine unchecked Exception, etwa eine NullPointerException, auf, deren Ursache fast unauffindbar ist. Wenn man eine Exception nicht fangen will, fängt man sie eben nicht, sondern deklariert sie mit throws. Wenn man sich sicher ist, dass eine Exception in diesem Fall nicht auftreten kann, fängt man sie und wirft in dem catch-Block eine neue unchecked Exception oder einen Error, die sind auch unchecked. Errors sind "irrecoverable" Zustände, wo nichts mehr zu retten ist, z.B. OutOfMemoryError, aber eben auch Zustände, die gar nicht passieren dürften.

    try
    {
      String eingabe = new BufferedReader(new InputStreamReader(System.in)).readLine();
    }
    catch(IOException ex)
    {
      throw new Error("Lesen von der Tastatur - da dürfte eigentlich gar nichts passieren", ex);
    }

Wenn man eine Exception fängt und eine neue Exception oder einen Error wirft (beides sind Untertypen von der Klasse Throwable), dann übergibt man dem Konstruktor außer der Fehlermeldung auch noch die alte Exception als Ursache (cause). Diese kann man später mit getCause() abfragen.

Mehrere Exceptions fangen

Schon im obigen Beispiel war zu sehen, dass man einfach mehrere Exceptions bei einer Methode deklarieren kann: "throws NumberFormatException, IOException". Man kann aber auch mehrere Exceptions fangen. Dazu schreibt man mehrere catch-Blöcke hintereinander. Nehmen wir das Beispiel ReadFile3, das sah so aus:

import java.io.*;

class ReadFile3
{
  public static void main(String[] args) throws IOException
  {
    FileReader fr = new FileReader("test.txt");
    BufferedReader br = new BufferedReader(fr);

    String zeile = "";

    while( (zeile = br.readLine()) != null )
    {
      System.out.println(zeile);
    }

    br.close();
  }
}

Nun wollen wir die IOException fangen, statt sie zu deklarieren. Der Konstruktor von FileReader kann eine FileNotFoundException werfen, welches eine spezielle Art von IOException ist. br.readLine() und br.close() können IOExceptions werfen, z.B. wenn der Benutzer die Datei nachträglich löscht oder den USB-Stick mit der Datei einfach rauszieht, während das Programm noch läuft. So sieht das Programm nun aus:

import java.io.*;

class MultiExceptions
{
  public static void main(String[] args)
  {
    try
    {
      FileReader fr = new FileReader("test.txt");
      BufferedReader br = new BufferedReader(fr);

      String zeile = "";

      while( (zeile = br.readLine()) != null )
      {
        System.out.println(zeile);
      }

      br.close();
    }
    catch(FileNotFoundException ex)
    {
      System.err.println("Datei nicht gefunden.");
    }
    catch(IOException ex)
    {
      System.err.println("Eingabe-Ausgabe-Fehler");
    }
  }
}

Man könnte beliebig viele weitere catch-Blöcke hinzufügen. Z.B. wenn in der Datei Zahlen stehen würden und man diese mit Integer.parseInt oder Double.parseDouble umwandeln würde, würde man noch einen catch-clause (catch-Block) für die NumberFormatException hinzufügen.

Speziellere Exceptions, d.h. Spezialfälle von allgemeineren Exception, muss man zuerst fangen, vor der allgemeineren. Da FileNotFoundException ein Untertyp von IOException ist, muss zuerst der catch-Block für FileNotFoundException und dann der für IOException stehen, sonst würde immer der von IOException ausgeführt werden, weil eben eine FileNotFoundException auch eine IOException ist. Wenn eine Datei nicht gefunden wird, werden nicht etwa beide catch-Blöcke ausgeführt, sondern nur der erste zutreffende.

Eine NumberFormatException wäre weder ein Unter- noch ein Obertyp für die FileNotFound- oder die IOException, also wäre es egal, ob ihr catch-Block als erstes, zweites oder drittes kommen würde.

finally

Manchmal gibt es "Aufräumarbeiten", die unbedingt durchgeführt werden müssen, auch wenn eine Exception passiert. Z.B. das Schließen eines Streams, insbesondere, wenn man in eine Datei geschrieben hat. Oder auch eine Datenbankverbindung korrekt beenden o.ä. So etwas schreibt man in einen finally-Block nach dem letzten catch-Block. Vielleicht fragst du dich jetzt, wozu das gut sein soll. Nachdem die Exception gefangen wurde, läuft das Programm doch weiter, da kann man diese Aufräumarbeiten doch einfach hinschreiben. Der erste Anwendungsfall ist, dass eine Exception gerade nicht gefangen, sondern deklariert wird. Dann hat man in der Methode einen try-finally-Block ohne catch dazwischen. Die zweite Variante ist, dass eine oder mehrere Exceptions gefangen werden, aber nicht alle. Z.B. IOException wird gefangen, NumberFormatException wird deklariert. Dann hat man try{...}catch(IOException ex){...}finally{Aufräumarbeiten}. Und hier sieht man auch gleich den Grund, warum man manchmal auch try-finally oder try-catch-finally hat, obwohl man keine Exception deklariert - manche Exceptions müssen ja nicht deklariert werden, z.B. NFE. Oder man könnte durch einen Programmierfehler eine ArrayIndexOutOfBoundsException verursachen. Etwa eine Datenbankverbindung sollte dennoch korrekt beendet werden. Deshalb schreibt man solche wichtigen Sachen oft in finally-Blöcke, auch wenn man eigentlich keine Exception erwartet. Eine weitere Anwendungsmöglichkeit ist beim vorzeitigen Beenden eines Schleifendurchlaufs mit continue, beim vorzeitigen Abbrechen einer Schleife mit break oder beim vorzeitigen Verlassen einer Methode mit return - wenn die Aufräumanweisungen oder etwa eine Inkrementanweisung in einer Schleife erst danach kommt, würde sie nicht ausgeführt werden. Auch da lässt sich finally sinnvoll einsetzen.

Als Beispiel für den zweiten Anwendungsfall ändern wir die Klasse AlterEingabeMitFehlerdeklarierung ein wenig ab, so dass IOException gefangen und nicht mehr deklariert wird.

import java.io.*;

class AlterEingabeMitFehlerdeklarierung2
{
  public static void main(String[] args)
  {
    System.out.print("Wie alt bist du: ");

    try
    {
      int alter = readPositiveNumber();

      if (alter >= 18)
      {
        System.out.println("Hier ist dein bestellter Wodka.");
      }
      else
      {
        System.out.println("Kein Wodka für dich.");
      }
    }
    catch(NumberFormatException ex)
    {
      System.err.println(ex);
    }

    System.out.println("Programmende");
  }

  static int readPositiveNumber() throws NumberFormatException
  {
    int zahl = 0;
    try
    {
      InputStreamReader isr = new InputStreamReader(System.in);
      BufferedReader br = new BufferedReader(isr);
      String eingabe = br.readLine();
      zahl = Integer.parseInt(eingabe);
      if (zahl < 0) throw new NumberFormatException("negative Zahl");
    }
    catch(IOException ex)
    {
      throw new Error("Das passiert sowieso nicht", ex);
    }
    finally
    {
      // hier würden jetzt wichtige Aufräumarbeiten stehen,
      // etwa br.close(), wenn wir nicht von der Tastatur lesen würden
    }
    return zahl;
  }
}

Die Variable zahl müssen wir vor dem try-Block deklarieren, weil wir sie nach dem try-catch-finally-Block noch verwenden wollen. Wenn wir sie innerhalb des try-Blocks deklariert hätten, würde sie nach dem Ende des try-Blocks nicht mehr existieren, weil sie "out of scope" wäre. Außerdem müssen wir sie mit irgendetwas initialisieren. Die Natur des try-Blocks ist es ja, dass er beim Auftreten eines Fehlers vorzeitig abbrechen könnte und möglicherweise gar nicht zu der Zeile zahl = Integer.parseInt(eingabe) kommt oder aber diese fehlschlägt. Dann wäre die Variable zahl nicht initialisiert und wir dürften sie nicht verwenden, also auch nicht zurückgeben.

Exceptions beim Lesen von der Tastatur

Zum Schluss nur kurz: BufferedReader.readLine() kann auch beim Lesen von System.in fehlschlagen, nämlich wenn System.in geschlossen wurde. Das passiert eigentlich nie, weil man System.in ja nicht schließen würde. Aber gerade einem Programmieranfänger könnte es passieren, dass er System.in versehentlich schließt, weil er einen darüberliegenden BufferedReader geschlossen hat:

    InputStreamReader isr = new InputStreamReader(System.in);
    BufferedReader br = new BufferedReader(isr);
    ... irgendwas einlesen ...
    br.close(); // schließt auch isr und System.in!
    ... später ...
    InputStreamReader isr2 = new InputStreamReader(System.in);
    BufferedReader br2 = new BufferedReader(isr);
    String eingabe = br.readLine(); // wirft IOException (stream closed)

In der nächsten Lektion geht es um Packages.