Fehlerbehandlung
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(); }