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.
- ArithmeticException
- 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.
- FileNotFoundException
- 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.
- RuntimeException
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.