Zum Hauptinhalt

Fehlerbehandlung

Website: Hamburg Open Online University
Kurs: Programmieren mit Java
Buch: Fehlerbehandlung
Gedruckt von: Gast
Datum: Dienstag, 3. Dezember 2024, 18:27

Beschreibung

Hier lernst Du, wie du mit Fehlern bei der Programmierung umgehst.

Gundlagen

Beim Programmieren und beim Ausführen eines Programms können die verschiedensten Fehler auftreten. Einige dieser Fehler können bereits während des Programmierens von der IDE erkannt werden. Dies sind zum Beispiel Syntaxfehler oder ein Aufruf von einer Methode, die nicht deklariert wurde. Fehler, die die Entwicklungsumgebung nicht erkennen kann, sind beispielsweise Fehler in der Programmlogik, also wenn der Programmierer schicht das falsche Programm schreibt. Die dritte Klasse von Fehlern sind die Laufzeitfehler, also Fehler, die erst auftreten während das Programm ausgeführt wird. Ein Laufzeitfehler wäre zum Beispiel, wenn man versucht in eine Datei zu schreiben, für die das Programm keine Schreibberechtigung hat.

Jeder hat schon einmal einen nicht behandelten Laufzeitfehler erlebt. Dies äußert sich dadurch, dass das Programm einfach abstürzt. Damit Java-Programme nicht abstürzen müssen, gibt es ein Fehlerbehandlungskonzept, dass es dem Programmierer ermöglicht Ausnahmen, die sogenannten Exception, im Programmfluss zu “behandeln”. Er kann hierfür einen try-Block um kritische Funktionsaufrufe schreiben. Danach folgt ein catch-Block. Tritt während der Ausführung des Programms eine Exception auf, so stoppt die Ausführung an genau dieser Stelle wie bei einem Programmabsturz. Sie wird aber anschließend im catch-Block fortgesetzt.

Im folgenden Beispiel verzichten wir auf die Prüfung, ob die Datei aus der wir lesen wollen existiert. Wir lesen einfach. Wenn die Datei dann nicht existiert, bricht das Programm ab und schreibt eine Fehlermeldung und läuft danach normal weiter.


File datei = new File("existiert-nicht.txt"); try { Scanner scnr = new Scanner(datei); while (scnr.hasNextLine()) { System.out.println(scnr.nextLine()); } scnr.close(); } catch (Exception e) { System.out.println("Datei konnte nicht gelesen werden: " + datei.getAbsolutePath()); }

Wie das Beispiel zeigt, wird beim Catch-Block, wie bei einer Methodendeklaration auch, ein Parameter definiert. In die dabei definierte Variable e wird ein Objekt übergeben, welches Informationen über die Fehlerursache liefert. Eine Beschreibung des Fehlers kann mit der getMessage() Methode abgefragt werden.

Als weitere wichtige Komponente enthält das Exception-Objekt den sogenannten Stacktrace. Hierbei handelt es sich um eine Auflistung der Methoden-Aufrufe, die zu dem Fehler geführt haben. Das Konzept des Stacks funktioniert so, dass jedes Mal wenn die Ausführung einer Methode begonnen wird, ein Eintrag hinzugefügt wird. Sobald dann das Ende dieser Methode erreicht wird, wird der Eintrag wieder vom Stack entfernt. Man kann sich dies wie einen Stapel vorstellen, auf den immer oben ein Element drauf gelegt wird und immer nur das oberste Element wieder entfernt werden kann. Wenn ein Fehler auftritt, ist das oberste Element im Stack also der Ort an dem der Fehler aufgetreten ist. Das erste beziehungsweise unterste Element auf dem Stack ist immer die Main-Methode.

Es ist immer sinnvoll den Stacktrace auszugeben, da er sehr wichtig für die Fehleranalyse und Programmverbesserung ist. Dies geschieht am einfachsten mit der Methode printStacktrace() die zum Beispie die folgende Ausgabe erzeugen könnte:


 java.io.FileNotFoundException: existiert-nicht.txt (No such file or directory)
     at java.base/java.io.FileInputStream.open0(Native Method)
     at java.base/java.io.FileInputStream.open(FileInputStream.java:219)
     at java.base/java.io.FileInputStream.<init>(FileInputStream.java:157)
     at java.base/java.util.Scanner.<init>(Scanner.java:639)
     at test.Main.leseDatei(Main.java:21)
     at test.Main.main(Main.java:11)
Die Ausgabe des Stacktrace beginnt mit einer Zeile in der der Fehler benannt wird. Sie beginnt mit dem Namen der Exceptionklasse, der einen ersten Hinweis auf den Ursprung des Problems gibt. Danach folgt eine Nachricht, die für diese spezielle Fehlerursache hinterlegt wurde. In den nachfolgenden Zeilen wird dann jeweils ein Funktionsaufruf dargestellt. Meistens genügt es, sich auf Zeilen zu konzentrieren, die mit dem eigenen Code zu tun haben. Um diese zu finden sucht man einfach nach Zeilen mit dem eigenen Package. In diesem Beispiel das Package “test”. Von diesen Zeilen ist die Oberste meist die entscheidende. In dieser Zeile meint “test”, das Package. Mit “Main” wird die Klasse benannt, in der der Fehler aufgetreten ist und “leseDatei” ist die Methode. In Klammern folgt noch einmal der Klassenname und die Zeile des Aufrufs.

Wenn man diese Information richtig verstanden hat, kann man schnell die Ursache eines Fehlers finden und beheben. Die optimale Korrektur für unseren Fehler wäre, vor dem Zugriff auf die Datei zu prüfen, ob sie existiert und lesbar ist.

Eine allgemeine Antwort, wie Fehler korrekt behandelt werden gibt es leider nicht. Als Richtwert kann man sagen, dass es besser ist Fehler zu vermeiden als sie nachträglich zu behandeln. Bei allen anderen Fehlern muss man überlegen, ob man den Zustand des Programms wieder so korrigieren kann, dass es ein korrektes Ergebnis liefert und diesen wieder herstellen. Wenn dies nicht möglich ist, sollte man nur eine aussagekräftige, verständliche Mitteilung für den Nutzer ausgeben und dann das Programm beenden.

In einigen Situationen ist es nicht möglich eine Exception sinnvoll zu behandeln. Für diesen Fall sieht Java die throws-Erweiterung bei der Methodendeklaration vor. Wir hatten sie in der Vergangenheit schon öfter benutzt, damit der Compiler unsere Methoden akzeptiert.

Manchmal ist es sinnvoll, dass spezieller Code in jedem Fall ausgeführt wird, unabhängig davon, ob nun ein Fehler aufgetreten ist oder nicht. Hierfür kann nach oder statt dem catch-Block ein finally-Block geschrieben werden. Was darin steht, wird in jedem Fall am Ende der Fehlerbehandlung ausgeführt.


try { leseDatei(new File("datei.txt")); } catch(Exception e) { // Fehler wird behandelt } finally { aufraeumen(); }

Hierarchie der Exceptions


Die Exceptions in Java sind durch verschiedene Klassen dargestellt. Diese Klassen wiederum sind in einer hierarchischen Struktur angeordnet. Es folgt eine Liste der für uns wichtigsten Exceptions.

  • Exception
     Die Klasse Exception ist die Basisklasse. Immer wenn man sagen möchte, dass man irgendeine beliebige Exception abfangen möchte so gibt man diesen Typ an. 
    • RuntimeException
       Die Ausprägungen dieser Klasse können während der Ausführung eines Programms theoretisch überall auftreten. Aus diesem Grund erzwingt der Compiler auch keine Behandlung dieser Exceptions. Man nennt dies “unchecked” Exceptions. Alle anderen Exceptions sind “checked” Exceptions, für die der Compiler eine Behandlung erzwingt. 
      • ArithmeticException
         Sie tritt auf, wenn das Programm eine Berechnung durchführen soll, die mathematisch nicht möglich ist. Zum Beispiel dividieren durch 0.
      • NullPointerException
         Eine NullPointerException tritt auf, wenn auf eine Eigenschaft oder eine Methode eines Objektes zugegriffen wird, das den Wert null hat.
      • IndexOutOfBoundsException
         Bei der Verwendung von Arrays kann es passieren, dass man auf einen Index zugreifen möchte, der zu groß oder kleiner als 0 ist. In diesen Fällen tritt eine IndexOutOfBoundsException auf.
    • IOException
       Alle IOExceptions signalisieren, dass bei der Eingabe oder der Ausgabe von Daten ein Fehler aufgetreten ist. 
      • FileNotFoundException
         Sie signalisiert, dass eine Datei nicht existiert.
      • EOFException
         Wird das Ende einer Eingabedatei unerwartet früh erreicht, so tritt eine EOFException auf.
    • ParseException
       Bei dem Versuch aus einem String zum Beispiel eine Zahl der ein Datum zu extrahieren kann es zu Fehlern kommen, wenn der String nicht korrekt formatiert ist.
Auch wenn wir in diesem Kurs damit auskommen nur die Klasse Exception zu behandeln, wollen wir noch kurz auf die Möglichkeiten schauen, die uns diese Hierarche bietet.

Bei der Deklaration von Methoden können nach dem throws Schlüsselwort mehrere Exception-Klassen durch Komma getrennt aufgeführt werden. So könnte man zum Beispiel angeben, dass ParseException und EOFException nicht behandeln wird. Der Compiler weiß dann aber, dass falls eine Datei geöffnet werden soll, er die Behandlung einer FileNotFoundException erzwingen soll.


 public static int readNumberFromFile(File file) throws ParseException, EOFException {
     try {
         // lese Dateiinhalt und wandle ihn in eine Zahl um
     } catch (FileNotFoundException e) {
         // verwende einen Standardwert
     }
 }

Das Beispiel zeigt, wie man dem Compiler mitteilt, wie man mit unterschiedlichen Exceptionklassen umgehen möchte.

Eine andere Möglichkeit ist, dass man in einem Try-Catch mehrere unterschiedliche Exceptionklassen behandeln kann. Dafür kann man mehrere catch-Blöcke aneinander reihen.


 try {
     leseDatei(datei);
 } catch(NullpointerException e) {
     // könnte auftreten, wenn datei null ist
 } catch(FileNotFound e) {
     // könnte auftreten wenn die Datei nicht existiert
 } catch(IOException e) {
     // könnte auftreten wenn andere Leseprobleme auftreten
 } catch(Exception e) {
     // fängt alle übrigen Fehler auf
 }
Im Beispiel ist zu sehen, wie mehrere unterschiedliche Exceptionklassen abgefangen werden können. Es ist dabei wichtig, dass zuerst die speziellen Fälle benannt werden und erst später die allgemeinen Fälle.

Exceptions erstellen


Exceptions werden nicht nur von Java selbst erstellt, wir können auch eigene Exceptions erstellen. Hierfür erstellt man eine Instanz der gewünschten Exceptionklasse und “wirft” diese mit Hilfe des throw Schlüsselwortes. Bei der Instanziierung kann man einen String als Nachricht mit übergeben.


 if(parameter < 0) {
     throw new RuntimeException("Der parameter darf nicht negativ sein.");
 }
Die Behandlung dieses Fehlers funktioniert dann wieder wie gehabt.