4.01 Fehlerbehandlung: Exceptions

Syntaktische Fehler vs Laufzeitfehler

Grundsätzlich müssen wir zwei Arten von Fehlern unterscheiden:

  • Syntaktische Fehler: Dies sind Fehler, die bereits beim Programmieren entstehen. Zum Beispiel ein falsches Setzen einer Klammer oder der Versuch einen boolean-Wert in einer int-Variablen zu speichern.
  • Laufzeitfehler: Dies sind Fehler, die während der Ausführung eines Programms auftreten. Beispielsweise eine Eingabe des Benutzers, die nicht zu dem passt, was verlangt wird oder der Versuch, eine Datei zu öffnen, die gar nicht vorhanden ist.

Faustregel: Syntaktische Fehler werden beim Programmieren von der Entwicklungsumgebung (wie Eclipse) rot angestrichen.

Syntaktische Fehler können (und müssen) wir natürlich als Programmierer vermeiden. Laufzeitfehler hingegen können wir niemals mit Sicherheit ausschließen, da sie durch Faktoren verursacht werden können, die wir nicht beeinflussen. Bei diesen können wir lediglich bestimmen, wie wir mit dem Auftreten eines solchen Fehlers umgehen.

Einfache Fehlerbehandlung mit if

In sehr einfachen Fällen können wir mit uns bereits bekannten Mitteln Laufzeitfehler auffangen. Sehen wir uns dieses Beispiel an:

import java.util.Scanner;

public class FehlerbehandlungMitIf {

  public static void main(String [] args){

    Scanner meinScanner = new Scanner(System.in);

    int erste;
    int zweite;
    int ergebnis;

    System.out.print("Wie lautet die erste Zahl? ");
    erste = meinScanner.nextInt();

    System.out.print("Wie lautet die zweite Zahl? ");
    zweite = meinScanner.nextInt();

    if (zweite == 0){
      System.err.println("Durch 0 darf man nicht teilen!");
    }else{
      ergebnis = erste/zweite;
      System.out.println(ergebnis);
    }

    meinScanner.close();

  }
}

Dieses einfache Programm fragt den Benutzer nach zwei Zahlen (genauer: nach zwei int-Werten) und teilt dann die erste durch die zweite. Sollte die zweite Zahl 0 sein, wird eine Fehlermeldung ausgegeben. Damit haben wir den wohl naheliegendsten Laufzeitfehler abgefangen.
Beachte, dass hier statt System.out.println die Anweisung System.err.println verwendet wird. Dadurch erscheint in der Konsole die Meldung in roter Schrift.

Fehlerbehandlung mit Exceptions

try-catch-finally

Bei sehr einfachen Fehlern wie oben kommt man also schon mit einem if aus. Allerdings kann es ein, dass wesentlich mehr Fehler denkbar wären, so dass wir dann mit entsprechend vielen if-Anweisungen arbeiten müssten. So etwas möchte man vermeiden, da ein solches Vorgehen vom Programmieren der eigentlichen Funktionen zu sehr ablenken würde.

Stattdessen verwendet man einen try-Block. Die Idee ist, dass der Programmtext, der einen Fehler auslösen könnte in so einen Block geschrieben wird ohne, dass man sich an dieser Stelle schon Gedanken über den Umgang mit den Fehlern macht. Man kann in diesem Block also erst einmal ganz „ungestört“ programmieren.

Anschließend folgt ein catch-Block. In diesem werden dann die Fehler behandelt.

Sehen wir uns ein Beispiel an:

import java.util.Scanner;

public class Fehlerbehandlung {

  public static void main(String [] args){

    Scanner meinScanner = new Scanner(System.in);

    int erste;
    int zweite;
    int ergebnis;

    try{
      System.out.print("Wie lautet die erste Zahl? ");
      erste = meinScanner.nextInt();
      System.out.print("Wie lautet die zweite Zahl? ");
      zweite = meinScanner.nextInt();
      
      ergebnis = erste/zweite;
      System.out.println(ergebnis);
      
    }catch(Exception e){
      System.err.println("Durch 0 darf man nicht teilen!");
    }

    meinScanner.close();

  }
}

Im try-Block wird das Problem der Division durch 0 zunächst ignoriert und die Rechnung wird einfach unbeirrt durchgezogen. Tritt dabei aber ein Fehler auf – der normalerweise das Programm abstürzen lassen würde – wird eine Instanz der Klasse Exception erzeugt und in den catch-Block gewechselt.

Diese Fehlerbehandlung ist allerdings noch recht ungenau. Nicht nur eine Eingabe einer Null bei der zweiten Zahl verursacht einen Fehler sondern auch eine Eingabe eines unerlaubten Zeichens. Um hier genauer unterscheiden zu können, gibt es verschiedene Unterklassen von Exception:

import java.util.InputMismatchException;
import java.util.Scanner;

public class Fehlerbehandlung {

  public static void main(String [] args){

    Scanner meinScanner = new Scanner(System.in);

    int erste;
    int zweite;
    int ergebnis;

    try{
      System.out.print("Wie lautet die erste Zahl? ");
      erste = meinScanner.nextInt();
      System.out.print("Wie lautet die zweite Zahl? ");
      zweite = meinScanner.nextInt();
      
      ergebnis = erste/zweite;
      System.out.println(ergebnis);
      
    }catch(ArithmeticException e){
      System.err.println("Durch 0 darf man nicht teilen!");
    }catch(InputMismatchException e){
      System.err.println("Unerlaubte Eingabe!");
    }catch(Exception e){
      System.err.println("Irgendetwas anderes ist schief gegangen.");
    }

    meinScanner.close();

  }
}

Wird nun eine Exception ausgelöst, wird der erste catch-Block mit einer passenden Unterklasse von Exception gesucht und ausgeführt. Die anderen Blöcke werden ignoriert. Daher sollte man die verschiedenen Blöcke von speziellen zu allgemeinen Exceptions sortieren – würde der erste Block allgemein eine Instanz von Exception abfangen, würde immer nur dieser ausgeführt.

Wir können auch noch neben try und catch auch noch einen finally-Block ergänzen. Die Anweisungen, die dort stehen, werden in jedem Fall ausgeführt – egal ob ein Fehler auftritt oder nicht:

import java.util.InputMismatchException;
import java.util.Scanner;

public class Fehlerbehandlung {

  public static void main(String [] args){

    Scanner meinScanner = new Scanner(System.in);

    int erste;
    int zweite;
    int ergebnis;

    try{
      System.out.print("Wie lautet die erste Zahl? ");
      erste = meinScanner.nextInt();
      System.out.print("Wie lautet die zweite Zahl? ");
      zweite = meinScanner.nextInt();
      
      ergebnis = erste/zweite;
      System.out.println(ergebnis);
      
    }catch(ArithmeticException e){
      System.err.println("Durch 0 darf man nicht teilen!");
    }catch(InputMismatchException e){
      System.err.println("Unerlaubte Eingabe!");
    }catch(Exception e){
      System.err.println("Irgendetwas anderes ist schief gegangen.");
    }finally{
      meinScanner.close();
      System.out.println("ENDE!");
    }

  }
}

throw – Eine Exception werfen

Wenn wir eine Methode aufrufen, die Exceptions auslösen kann, können wir natürlich innerhalb der Methode mit try-catch-finally arbeiten. Alternativ können wir aber auch einfach die Exception an die aufrufende Methode weiterreichen. Das bietet sich zum Beispiel an, wenn verschiedene Methoden ähnliche Fehler verursachen können:

import java.util.InputMismatchException;
import java.util.Scanner;

public class Fehlerbehandlung {

  public static void main(String [] args){

    Scanner meinScanner = new Scanner(System.in);

    int erste;
    int zweite;

    try{
      System.out.print("Wie lautet die erste Zahl? ");
      erste = meinScanner.nextInt();
      System.out.print("Wie lautet die zweite Zahl? ");
      zweite = meinScanner.nextInt();
      
      System.out.println(dividieren(erste, zweite));
      System.out.println(subtrahieren(erste, zweite));
      
    }catch(ArithmeticException e){
      System.err.println(e.getMessage());
    }catch(InputMismatchException e){
      System.err.println("Unerlaubte Eingabe!");
    }catch(Exception e){
      System.err.println("Irgendetwas anderes ist schief gegangen.");
    }finally{
      meinScanner.close();
      System.out.println("ENDE!");
    }
    
    
  }
  
  private static int dividieren(int a, int b) throws ArithmeticException{
    if (b == 0){
      throw new ArithmeticException("Durch 0 darf man nicht teilen.");
    }else{
      return a/b;
    }
  }
  
  private static int subtrahieren(int a, int b) throws ArithmeticException{
    if (b>a){
      throw new ArithmeticException("Auf negative Ergebnisse habe ich keine Lust.");
    }else{
      return a-b;
    }		
  }
    
}

In den beiden Untermethoden prüfen wir mit if, ob ein Fehler vorliegt – wobei der in der zweiten Methode natürlich nur der Veranschaulichung dient. Liegt ein Problem vor, wird eine Instanz von ArithmeticException erzeugt und an die main-Methode gegeben. Diese muss dann mit dieser Exception umgehen.

Beim Erzeugen der Exception haben wir hier noch eine Message angegeben. Diese wird in der main-Methode mit getMessage ausgelesen.