| author | André Matutat & Carsten Gips (HSBI) |
|---|---|
| title | Exception-Handling |
::: tldr Man unterscheidet in Java zwischen Exceptions und Errors. Ein Error ist ein Fehler im System (OS, JVM), von dem man sich nicht wieder erholen kann. Eine Exception ist ein Fehlerfall innerhalb des Programmes, auf den man innerhalb des Programms reagieren kann.
Mit Hilfe von Exceptions lassen sich Fehlerfälle im Programmablauf deklarieren und
behandeln. Methoden können/müssen mit dem Keyword throws gefolgt vom Namen der
Exception deklarieren, dass sie im Fehlerfall diese spezifische Exception werfen
(und nicht selbst behandeln).
Zum Exception-Handling werden die Keywords try, catch und finally verwendet.
Dabei wird im try-Block der Code geschrieben, der einen potenziellen Fehler wirft.
Im catch-Block wird das Verhalten implementiert, dass im Fehlerfall ausgeführt
werden soll, und im finally-Block kann optional Code geschrieben werden, der
sowohl im Erfolgs- als auch Fehlerfall ausgeführt wird.
Es wird zwischen checked Exceptions und unchecked Exceptions unterschieden. Checked Exceptions sind für erwartbare Fehlerfälle gedacht, die nicht vom Programm ausgeschlossen werden können, wie das Fehlen einer Datei, die eingelesen werden soll. Checked Exceptions müssen deklariert oder behandelt werden. Dies wird vom Compiler überprüft.
Unchecked Exceptions werden für Fehler in der Programmlogik verwendet, etwa das
Teilen durch 0 oder Index-Fehler. Sie deuten auf fehlerhafte Programmierung,
fehlerhafte Logik oder beispielsweise mangelhafte Eingabeprüfung in. Unchecked
Exceptions müssen nicht deklariert oder behandelt werden. Unchecked Exceptions
leiten von RuntimeException ab.
Als Faustregel gilt: Wenn der Aufrufer sich von einer Exception-Situation erholen kann, sollte man eine checked Exception nutzen. Wenn der Aufrufer vermutlich nichts tun kann, um sich von dem Problem zu erholen, dann sollte man eine unchecked Exception einsetzen. :::
::: youtube
- VL Exceptions :::
int div(int a, int b) {
return a / b;
}
div(3, 0);::: notes Problem: Programm wird abstürzen, da durch '0' geteilt wird ... :::
Optional<Integer> div(int a, int b) {
if (b == 0) return Optional.empty();
return Optional.of(a / b);
}
Optional<Integer> x = div(3, 0);
if (x.isPresent()) {
// do something
} else {
// do something else
}::: notes Probleme:
- Da
intnichtnullsein kann, muss einIntegerObjekt erzeugt und zurückgegeben werden: Overhead wg. Auto-Boxing und -Unboxing! - Der Aufrufer muss auf
nullprüfen. - Es wird nicht kommuniziert, warum
nullzurückgegeben wird. Was ist das Problem? - Was ist, wenn
nullein gültiger Rückgabewert sein soll? :::
[[Hinweis: checked vs. unchecked]{.ex}]{.slides}
::: notes
Error:- Wird für Systemfehler verwendet (Betriebssystem, JVM, ...)
StackOverflowErrorOutOfMemoryError
- Von einem Error kann man sich nicht erholen
- Sollten nicht behandelt werden
- Wird für Systemfehler verwendet (Betriebssystem, JVM, ...)
Exception:- Ausnahmesituationen bei der Abarbeitung eines Programms
- Können "checked" oder "unchecked" sein
- Von Exceptions kann man sich erholen
- "Checked" Exceptions:
- Für erwartbare Fehlerfälle, deren Ursprung nicht im Programm selbst liegt
FileNotFoundExceptionIOException
- Alle nicht von
RuntimeExceptionableitende Exceptions - Müssen entweder behandelt (
try/catch) oder deklariert (throws) werden: Dies wird vom Compiler überprüft!
- Für erwartbare Fehlerfälle, deren Ursprung nicht im Programm selbst liegt
- "Unchecked" Exceptions:
- Logische Programmierfehler ("Versagen" des Programmcodes)
IndexOutOfBoundExceptionNullPointerExceptionArithmeticExceptionIllegalArgumentException
- Leiten von
RuntimeExceptionoder Unterklassen ab - Müssen nicht deklariert oder behandelt werden
- Logische Programmierfehler ("Versagen" des Programmcodes)
Beispiele checked Exception:
- Es soll eine Abfrage an eine externe API geschickt werden. Diese ist aber aktuell nicht zu erreichen. "Erholung": Anfrage noch einmal schicken.
- Es soll eine Datei geöffnet werden. Diese ist aber nicht unter dem angegebenen Pfad zu finden oder die Berechtigungen stimmen nicht. "Erholung": Aufrufer öffnet neuen File-Picker, um es noch einmal mit einer anderen Datei zu versuchen.
Beispiele unchecked Exception:
- Eine
for-Loop über ein Array ist falsch programmiert und will auf einen Index im Array zugreifen, der nicht existiert. Hier kann der Aufrufer nicht Sinnvolles tun, um sich von dieser Situation zu erholen. - Argumente oder Rückgabewerte einer Methode können
nullsein. Wenn man das nicht prüft, sondern einfach Methoden auf dem vermeintlichen Objekt aufruft, wird eineNullPointerExceptionausgelöst, die eine Unterklasse vonRuntimeExceptionist und damit eine unchecked Exception. Auch hier handelt es sich um einen Fehler in der Programmlogik, von dem sich der Aufrufer nicht sinnvoll erholen kann. :::
int div(int a, int b) throws ArithmeticException {
return a / b;
}::: notes Alternativ: :::
\bigskip
int div(int a, int b) throws IllegalArgumentException {
if (b == 0) throw new IllegalArgumentException("Can't divide by zero");
return a / b;
}::: notes Exception können an an den Aufrufer weitergeleitet werden oder selbst geworfen werden.
Wenn wie im ersten Beispiel bei einer Operation eine Exception entsteht und nicht
gefangen wird, dann wird sie automatisch an den Aufrufer weitergeleitet. Dies wird
über die throws-Klausel deutlich gemacht (Keyword throws plus den/die Namen der
Exception(s), angefügt an die Methodensignatur). Bei unchecked Exceptions kann man
das tun, bei checked Exceptions muss man dies tun.
Wenn man wie im zweiten Beispiel selbst eine neue Exception werfen will, erzeugt man
mit new ein neues Objekt der gewünschten Exception und "wirft" diese mit throw.
Auch diese Exception kann man dann entweder selbst fangen und bearbeiten (siehe
nächste Folie) oder an den Aufrufer weiterleiten und dies dann entsprechend über die
throws-Klausel deklarieren: nicht gefangene checked Exceptions müssen deklariert
werden, nicht gefangene unchecked Exceptions können deklariert werden.
Wenn mehrere Exceptions an den Aufrufer weitergeleitet werden, werden sie in der
throws-Klausel mit Komma getrennt: throws Exception1, Exception2, Exception3.
Anmerkung: In beiden obigen Beispielen wurde zur Verdeutlichung, dass die
Methode div() eine Exception wirft, diese per throws-Klausel deklariert. Da es
sich bei den beiden Beispielen aber jeweils um unchecked Exceptions handelt, ist
dies im obigen Beispiel nicht notwendig. Der Aufrufer muss auch nicht ein
passendes Exception-Handling einsetzen!
Wenn wir stattdessen eine checked Exception werfen würden oder in div() eine
Methode aufrufen würden, die eine checked Exception deklariert hat, muss diese
checked Exception entweder in div() gefangen und bearbeitet werden oder aber per
throws-Klausel deklariert werden. Im letzteren Fall muss dann der Aufrufer
analog damit umgehen (fangen oder selbst auch deklarieren). Dies wird vom Compiler
geprüft!
:::
[[Hinweis: throws und checked vs. unchecked]{.ex}]{.slides}
int a = getUserInput();
int b = getUserInput();
try {
div(a, b);
} catch (IllegalArgumentException e) {
e.printStackTrace(); // Wird im Fehlerfall aufgerufen
}
// hier geht es normal weiter::: notes
- Im
tryBlock wird der Code ausgeführt, der einen Fehler werfen könnte. - Mit
catchkann eine Exception gefangen und imcatchBlock behandelt werden.
Anmerkung: Das bloße Ausgeben des Stacktrace via e.printStackTrace() ist noch
kein sinnvolles Exception-Handling! Hier sollte auf die jeweilige Situation
eingegangen werden und versucht werden, den Fehler zu beheben oder dem Aufrufer
geeignet zu melden!
:::
try {
someMethod(a, b, c);
} catch (IllegalArgumentException iae) {
iae.printStackTrace();
} catch (FileNotFoundException | NullPointerException e) {
e.printStackTrace();
}::: notes
Eine im try-Block auftretende Exception wird der Reihe nach mit den
catch-Blöcken gematcht (vergleichbar mit switch case).
Wichtig: Dabei muss die Vererbungshierarchie beachtet werden. Die
spezialisierteste Klasse muss ganz oben stehen, die allgemeinste Klasse als letztes.
Sonst wird eine Exception u.U. zu früh in einem nicht dafür gedachten catch-Zweig
aufgefangen.
Wichtig: Wenn eine Exception nicht durch die catch-Zweige aufgefangen wird,
dann wird sie an den Aufrufer weiter geleitet. Im Beispiel würde eine IOException
nicht durch die catch-Zweige gefangen (IllegalArgumentException und
NullPointerException sind im falschen Vererbungszweig, und FileNotFoundException
ist spezieller als IOException) und entsprechend an den Aufrufer weiter gereicht.
Da es sich obendrein um eine checked Exception handelt, müsste man diese per
throws IOException an der Methode deklarieren.
:::
[[Hinweis: catch und Vererbungshierarchie]{.ex}]{.slides}
Scanner myScanner = new Scanner(System.in);
try {
return 5 / myScanner.nextInt();
} catch (InputMismatchException ime) {
ime.printStackTrace();
} finally {
// wird immer aufgerufen
myScanner.close();
}::: notes
Der finally Block wird sowohl im Fehlerfall als auch im Normalfall aufgerufen.
Dies wird beispielsweise für Aufräumarbeiten genutzt, etwa zum Schließen von
Verbindungen oder Input-Streams.
:::
try (Scanner myScanner = new Scanner(System.in)) {
return 5 / myScanner.nextInt();
} catch (InputMismatchException ime) {
ime.printStackTrace();
}::: notes
Im try-Statement können Ressourcen deklariert werden, die am Ende sicher
geschlossen werden. Diese Ressourcen müssen java.io.Closeable implementieren.
:::
// Checked Exception
public class MyCheckedException extends Exception {
public MyCheckedException(String errorMessage) {
super(errorMessage);
}
}\bigskip
// Unchecked Exception
public class MyUncheckedException extends RuntimeException {
public MyUncheckedException(String errorMessage) {
super(errorMessage);
}
}::: notes
Eigene Exceptions können durch Spezialisierung anderer Exception-Klassen realisiert
werden. Dabei kann man direkt von Exception oder RuntimeException ableiten oder
bei Bedarf von spezialisierteren Exception-Klassen.
Wenn die eigene Exception in der Vererbungshierarchie unter RuntimeException
steht, handelt es sich um eine unchecked Exception, sonst um eine checked
Exception.
In der Benutzung (werfen, fangen, deklarieren) verhalten sich eigene Exception-Klassen wie die Exceptions aus dem JDK. :::
int getFirstLineAsInt(String pathToFile) {
FileReader fileReader = new FileReader(pathToFile);
BufferedReader bufferedReader = new BufferedReader(fileReader);
String firstLine = bufferedReader.readLine();
return Integer.parseInt(firstLine);
}[Zeigen: exceptions.HowMuchTry]{.ex href="https://github.com/Programmiermethoden-CampusMinden/PM-Lecture/blob/master/markdown/java-jvm/src/exceptions/HowMuchTry.java"}
::: notes Hier lassen sich verschiedene "Ausbaustufen" unterscheiden.
int getFirstLineAsIntV1(String pathToFile) throws FileNotFoundException, IOException {
FileReader fileReader = new FileReader(pathToFile);
BufferedReader bufferedReader = new BufferedReader(fileReader);
String firstLine = bufferedReader.readLine();
return Integer.parseInt(firstLine);
}Der Aufrufer hat den Pfad als String übergeben und ist vermutlich in der Lage, auf
Probleme mit dem Pfad sinnvoll zu reagieren. Also könnte man in der Methode selbst
auf ein try/catch verzichten und stattdessen die FileNotFoundException (vom
FileReader) und die IOException (vom bufferedReader.readLine()) per throws
deklarieren.
Anmerkung: Da FileNotFoundException eine Spezialisierung von IOException ist,
reicht es aus, lediglich die IOException zu deklarieren.
int getFirstLineAsIntV2(String pathToFile) {
FileReader fileReader = null;
try {
fileReader = new FileReader(pathToFile);
} catch (FileNotFoundException fnfe) {
fnfe.printStackTrace(); // Datei nicht gefunden
}
BufferedReader bufferedReader = new BufferedReader(fileReader);
String firstLine = null;
try {
firstLine = bufferedReader.readLine();
} catch (IOException ioe) {
ioe.printStackTrace(); // Datei kann nicht gelesen werden
}
try {
return Integer.parseInt(firstLine);
} catch (NumberFormatException nfe) {
nfe.printStackTrace(); // Das war wohl kein Integer
}
return 0;
}In dieser Variante wird jede Operation, die eine Exception werfen kann, separat in
ein try/catch verpackt und jeweils separat auf den möglichen Fehler reagiert.
Dadurch kann man die Fehler sehr einfach dem jeweiligen Statement zuordnen.
Allerdings muss man nun mit Behelfsinitialisierungen arbeiten und der Code wird sehr in die Länge gezogen und man erkennt die eigentlichen funktionalen Zusammenhänge nur noch schwer.
Anmerkung: Das "Behandeln" der Exceptions ist im obigen Beispiel kein gutes Beispiel für das Behandeln von Exceptions. Einfach nur einen Stacktrace zu printen und weiter zu machen, als ob nichts passiert wäre, ist kein sinnvolles Exception-Handling. Wenn Sie solchen Code schreiben oder sehen, ist das ein Anzeichen, dass auf dieser Ebene nicht sinnvoll mit dem Fehler umgegangen werden kann und dass man ihn besser an den Aufrufer weiter reichen sollte (siehe nächste Folie).
int getFirstLineAsIntV3(String pathToFile) {
try {
FileReader fileReader = new FileReader(pathToFile);
BufferedReader bufferedReader = new BufferedReader(fileReader);
String firstLine = bufferedReader.readLine();
return Integer.parseInt(firstLine);
} catch (FileNotFoundException fnfe) {
fnfe.printStackTrace(); // Datei nicht gefunden
} catch (IOException ioe) {
ioe.printStackTrace(); // Datei kann nicht gelesen werden
} catch (NumberFormatException nfe) {
nfe.printStackTrace(); // Das war wohl kein Integer
}
return 0;
}Hier wurde der eigentliche funktionale Kern der Methode in ein gemeinsames
try/catch verpackt und mit einem mehrstufigen catch auf die einzelnen Fehler
reagiert. Durch die Art der Exceptions sieht man immer noch, wo der Fehler herkommt.
Zusätzlich wird die eigentliche Funktionalität so leichter erkennbar.
Anmerkung: Auch hier ist das gezeigte Exception-Handling kein gutes Beispiel. Entweder man macht hier sinnvollere Dinge, oder man überlässt dem Aufrufer die Reaktion auf den Fehler. :::
private static void methode1(int x) throws IOException {
JFileChooser fc = new JFileChooser();
fc.showDialog(null, "ok");
methode2(fc.getSelectedFile().toString(), x, x * 2);
}
private static void methode2(String path, int x, int y) throws IOException {
FileWriter fw = new FileWriter(path);
BufferedWriter bw = new BufferedWriter(fw);
bw.write("X:" + x + " Y: " + y);
}
public static void main(String... args) {
try {
methode1(42);
} catch (IOException ioe) {
ioe.printStackTrace();
}
}::: notes
Prinzipiell steht es einem frei, wo man eine Exception fängt und behandelt. Wenn im
main() eine nicht behandelte Exception auftritt (weiter nach oben geleitet wird),
wird das Programm mit einem Fehler beendet.
Letztlich scheint es eine gute Idee zu sein, eine Exception so nah wie möglich am Ursprung der Fehlerursache zu behandeln. Man sollte sich dabei die Frage stellen: Wo kann ich sinnvoll auf den Fehler reagieren? :::
- Für erwartbare Fehlerfälle, deren Ursprung nicht im Programm selbst liegt
- Aufrufer kann sich von der Exception erholen
\bigskip
- Logische Programmierfehler ("Versagen" des Programmcodes)
- Aufrufer kann sich von der Exception vermutlich nicht erholen
::: notes Vergleiche "Unchecked Exceptions --- The Controversy". :::
ErrorundException: System vs. Programm- Checked und unchecked Exceptions:
Exceptionvs.RuntimeException
\smallskip
try: Versuche Code auszuführencatch: Verhalten im Fehlerfallfinally: Verhalten im Erfolgs- und Fehlerfall
\smallskip
throw: Wirft eine Exceptionthrows: Deklariert eine Exception an Methode
\smallskip
- Eigene Exceptions durch Ableiten von anderen Exceptions [(werden je nach Vererbungshierarchie automatisch checked oder unchecked)]{.notes}
::: readings
- @LernJava
- @Ullenboom2021 [Kap. 8]
- @Java-SE-Tutorial :::
::: outcomes
- k2: Unterschied zwischen Error und Exception
- k2: Unterschied zwischen checked und unchecked Exceptions
- k3: Umgang mit Exceptions
- k3: Eigene Exceptions schreiben :::
::: quizzes
::: challenges Betrachten Sie die Vorgaben.
Verbessern Sie das Exception-Handling
Im package better_try_catch finden Sie die Klasse BetterTryCatchMain, in der
verschiedene Methoden der Klasse MyFunctions aufgerufen werden.
Erklären Sie, warum das dort implementierte Exception-Handling nicht gut ist und verbessern Sie es.
Checked vs. unckecked Exceptions
Erklären Sie den Unterschied zwischen checked und unchecked Exceptions.
Im Folgenden werden verschiedene Exceptions beschrieben. Erklären Sie, ob diese jeweils "checked" oder "unchecked" sein sollten.
IntNotBetweenExceptionsoll geworfen werden, wenn ein Integer-Parameter nicht im definierten Wertebereich liegt.NoPicturesFoundExceptionsoll geworfen werden, wenn in einem übergebenen Verzeichnis keine Bilddateien gefunden werden konnten.NotAPrimeNumberExceptionsoll geworfen werden, wenn eine vom User eingegebene Zahl keine Primzahl ist.
Freigeben von Ressourcen
Im Package finally_resources finden Sie die Klasse MyResource.
Rufen Sie die Methode MyResource#doSomething auf, im Anschluss müssen Sie
immer die Methode MyResource#close aufrufen.
- Zeigen Sie den Aufruf mit
try-catch-finally. - Verändern Sie die Vorgaben so, dass Sie den Aufruf mit der "try-with-resources"-Technik ausführen können.
Where to catch?
Erklären Sie, wann und wo eine Exception gefangen und bearbeitet werden sollte.
Im Package where_to_catch finden Sie die Klasse JustThrow. Alle Methoden in der
Klasse werfen aufkommende Exceptions bis zur main hoch.
Verändern Sie die Vorgaben so, dass die Exceptions an den passenden Stellen gefangen und sinnvoll bearbeitet werden. Begründen Sie Ihre Entscheidungen. :::
