Runtime Exceptions
🚧 | Diese Seite befindet sich in Bearbeitung | 🚧 |
🤓 | Diese Seite ist eine Bewertungsrichtlinie, die ab Blatt 2 annotiert und ab Blatt 3 abgezogen wird. | 🤓 |
Beschreibung
Wir beziehen uns in diesem Artikel auf die Oracle Dokumentation.
In Java haben wir die Möglichkeit, entweder selbst Fehler im Quellcode zu überprüfen oder Exceptions für uns Arbeiten zu lassen. Insbesondere Runtime-Exceptions (Unchecked Exceptions).
Was machen die?
Unchecked-Exception bekommen ihren Namen daher, dass der Compiler keine Fehlermeldungen an entsprechenden fehlerbehafteten Stellen wirft, sondern erst dann, wenn während das Programm läuft, tatsächlich ein Fehler auftritt.
Warum ist das jetzt so schlimm? Ist doch weniger Aufwand und weniger nervig, sich um die ganzen Fehlermeldungen zu kümmern - selbst Schuld wenn die Eingabe fehlerhaft ist.
Es ist für die entwickelnde Person tatsächlich etwas weniger Aufwand und generell einfacher, nur Unchecked Exceptions zu verwenden. Allerdings führt das nicht nur zu Problemen in dem Ausführen des Programms, sondern auch in der Verwendung des Quellcodes durch andere. Diese müssen wissen, dass die Exception geworfen werden wird, um diese entsprechend zu behandeln. Wir kommen also nicht darum herum, einen fehlerhaften Zustand zu erkennen und beheben.
Aber wenn wir doch ohnehin alles Dokumentieren, warum dann nicht auch die Unchecked Exceptions, die eine Methode werfen kann?
Unchecked Exceptions repräsentieren in der Regel einen Programmierfehler, also einen Fehler, der durch die schreibende Person eigentlich durch entsprechende "Guard-Clauses" (in der Regel if-Anweisungen) erkannt und behandelt werden soll. Wir können nicht von der API erwarten, dass diese Fehler sinnvoll erkannt und behoben werden. Beispiele dafür sind das Teilen durch 0, referenzieren von Objekten durch eine Null-Referenz oder Indizierungsfehler in einem Array.
Da diese Fehler sehr variabel und oft auftreten können, müssen wir viele Unechecked Exceptions einfügen und dokumentieren. Das sorgt dafür, dass sehr schnell sehr unklar wird, was unser Quellcode eigentlich bezwecken soll (Informationsflut).
Eine Stelle, an der es Konvention ist, eine Runtime Exception zu fangen, ist, wenn eine Methode inkorrekt aufgerufen wird. Beispielsweise, wenn eines der Argumente null ist. Entsprechend wird dann eine NullPointerException geworfen (also eine Unchecked Exception).
Generell gilt:
Es soll keine RuntimeException geworfen oder Unterklassen der RuntimeException erstellt werden.
"If a client can reasonable be expected to recover from an exception, make it a checked exception. If a client cannot do anything to recover from the exception, make it an unchecked exception." - Oracle
Wie vermeide ich dann diese unchecked Exceptions?
- NullPointerException: Generell sollten null-Werte im Code vermieden werden. Kann eine Variable oder ein Attribut dennoch null sein, so muss vor jedem Zugriff ein Null-Check durchgeführt werden. Also: if(foo != null) {}. Der Fall, dass der Wert null ist, sollte angemessen behandelt werden.
- IndexOutOfBoundsException: Ist durch den vorhandenen Code (z.B. die Abbruchbedingung in for-Schleifen) nicht sichergestellt, dass jeder Indexzugriff auf Arrays und Listen valide ist, so muss der Index vor dem Zugriff gegen 0 und die Länge des Arrays verglichen werden. Vorsicht: Arrays und Listen sind in Java 0-indiziert. array[0] ist also valide, array[array.length] aber nicht.
- IllegalArgumentException: Der JavaDoc-Kommentar jeder (nicht privaten) Methode erläutert, welche Parameterwerte erlaubt sind. Entsprechend muss vor dem Aufruf der Methode sichergestellt werden, dass alle Einschränkungen eingehalten werden.
- ArithmeticException: Diese Exception wird typischerweise geworfen, wenn durch 0 geteilt wird. Eine entsprechende if-Abfrage vermeidet die Exception.
Achtung bei Streams:
Nur für diejenigen, die sich mit Streams schon auskennen (wird nicht in der Vorlesung behandelt): Innerhalb von Streams ist es nicht möglich, Checked Exceptions zu werfen, falls ein Fehler auftritt. Also eben nur unchecked Exceptions. Das ist auch ein beliebter Umweg. Diese können dann beispielsweise außerhalb des Streams gefangen werden. Aber auch hier gilt die oben genannte Regel: Das Fangen von Unchecked Exceptions ist nicht erlaubt. Eine Alternative wäre, mit Optionals (java.util.Optional) zu arbeiten. Allerdings sollten komplexe Operationen bei denen Fehler auftreten können, nicht in Streams durchgeführt werden. Jede Stream-Operation sollte trivial sein.
Ausnahme: Integer::parseInt()
Als einzige Ausnahme gibt es die NumberFormatException die von der Integer.parseInt() Methode geworfen werden kann. Diese ist die einzige unchecked Exception die gefangen werden darf und soll.
Negativbeispiel
public void iterate(int[] array) {
try {
int i = 0;
while (true) {
// Throws an Exception if i == array.length
doSomething(array[i], i);
i++;
}
} catch (ArrayIndexOutOfBoundsException e) {
// Reached the end of the array
}
}
public void dealDamage(Player player) {
try {
player.takeDamage(this.attackStrength);
} catch (NullPointerException e) {
Terminal.printLine("player must not be null");
}
}
Positivbeispiel
public void iterate(int[] array) {
// The for loop asserts that 0 <= i < array.length is always true
for (int i = 0; i < array.length; i++) {
doSomething(array[i], i);
}
}
public void dealDamage(Player player) {
if (player == null) {
throw new IllegalArgumentException("player must not be null");
}
player.takeDamage(this.attackStrength);
}