Exceptionhandling überrascht mich

Ich dachte eigentlich, ich weiß, wie Excpetions funktionieren, aber nöööööö…

import java.io.File;
import java.io.IOException;

public class ExceptionTest {

    static String foo() throws IOException {
        try {
            return new File("blubb").getCanonicalPath(); //may throw IOException
        } catch (Exception e) {
            e.printStackTrace();
            throw e;
        }
    }

    public static void main(String[] args) throws IOException{
        System.out.println(foo());
    }
}

Ich bin überrascht, dass der Compiler so clever ist throw e; zu akzeptieren, obwohl e ja als Exception deklariert ist, die Methode aber nur eine IOException erwartet. Natürlich funktioniert das ganze nicht mehr, wenn innerhalb des try-Blocks eine andere Exception geworfen werden könnte.

Order anders ausgedrückt: Wenn sich die aufgerufenen Methoden ändern und z.B. auf einmal SqlException geworfen wird, geht auf einmal foo() kaputt, obwohl wir im try ja “alle” Exceptions fangen. Alles immer noch sicher, aber doch ziemlich subtil.

Ich fände es besser, wenn dieser Code so nicht compilieren würde, und ich stattdessen catch(IOException e) schreiben müsste, das würde genauso funktionieren und wäre weniger überraschend (Principle of Least Surprise).

Oder was meint ihr?

1 „Gefällt mir“

Tatsache, ich hab damit jetzt ein bisschen rumgespielt: Er markiert dann “throw e;” als Fehler “Unhandled Exceptions: [NotIOException1], [NotIOException2]”. Obwohl e vom übergeordneten Typ Exception ist, weiß er ganz genau um welche Typen es sich handelt.

Ich stimme dir da zu, wobei ich vermute das es etwas mit den RuntimeExceptions zu tun hat die selber vom Typ Exception sind aber gänzlich anders behandelt werden. Deshalb wird er wahrscheinlich den Typ der Exceptions genauer analysieren um zu sehen ob es sich um einen RuntimeException handelt, dann wäre das wiederum eben kein Fehler.

„catch (Throwable e)“ wird noch viel spannender - zumindest, wenn man dann „throw e“ vergisst. Aber wenigstens gibt es dann keine unerwarteten Programmabrüche mehr. :smiley:

Natürlich hat das was mit dem Exceptionhandling zu tun. Wäre bei Throw (also bei der Abarbeitung) die Vererbungshierarchie (sozusagen) nicht umgekehrt, gäbe es ein Problem mit Error und RuntimeException, die man dann auch abfangen dürfte - nach jeder Anweisung versteht sich.

Um das nochmal zu vertiefen…

Viel besser fände ich es, wenn in Methodensignaturen Dinge wie “throws Throwable” (das allen Voran! Siehe “finalize()”), “throws Error” und “throws RuntimeException” nicht kompilieren würden, denn die Anweisung “throw” prüft ja auf Error, RuntimeException, Liste der “throws”-Anweisung in genau dieser Reihenfolge und Throwable ist ohnehin alles. Das ist doch auch der Grund, warum man nur bestimmte XxxExceptions abfangen soll und nicht gerade die, di in der “throws”-Liste stehen, oder sehe ich das falsch?

CloneNotSupportedException als checked Exception ist auch son Ding. Entweder man implementiert Cloneable, dann ist es cloneable oder man implementiert es nicht, dann ist es das eben nicht. So muss man sie abfangen und eine RuntimeException oder einen Error daraus machen, wenn man sein Klientel, das Clone-Methoden seiner APIs aufruft, nicht mit Exceptions nerven will.

Man soll RuntimeErrors nicht abfangen dürfen, oder nicht in der Methode als throws deklarieren dürfen? Letzteres ist in jeder Methode implizit. RuntimeErrors auf der anderen Seite fange ich relativ häufig ab, finde es dekadent wenn ein Library Entwickler seine Methode so schreibt dass das ganze Programm abstürzt wenn mal was nicht richtig funktioniert. Gerade in Netzwerkanwendungen will ich nicht dass durch falsche Signale meine Anwendung von Außen zum Absturz gebracht werden kann.

Kommt drauf an. Wenn es dokumentiert ist, dass die RuntimeException fliegen kann - dann finde ich das absolut Ok. Wobei ich aber auch generell kein wirklicher Freund von Checked-Exceptions bin (vor allem, weil diese in Java viel zu exzessiv verwendet werden und da lob ich mir C# wo es afaik nur unchecked exceptions gibt). Richtig schön werden diese in lambdas. Denn spätestens hier, kann man die nicht mehr weiterwerfen. Also ist man gezwungen sich um etwas zu kümmern, was an der Stelle u.U. nicht sinnvoll ist.

Also um RuntimeExceptions ging es mir hier gar nicht, die sind ja für den Checked-Exception-Mechanismus unsichtbar. Ich finde spannend, dass man einem try-catch-Block wie oben nicht ansehen kann, welche Exceptions er am Ende sozusagen “durchlässt”, also ob er z.B. in einer bestimmten Methode mit throws-Klausel zulässig ist oder nicht. Wenn jemand im Beispiel an getCanonicalPath rumschrauben würde (sagen wir, auf throws SqlException ändern), würde er auf sehr subtile Weise Code in einer ganz anderen Klasse brechen, das ist der Punkt, der mir seltsam vorkommt.

Wenn es nicht um Exceptions ginge, wäre das Äquivalent ungefähr sowas:

Number i = Integer.valueOf(42); 
Integer j = i;

Das könnte der Compiler ja auch erlauben, weil sicher ist, dass i immer ein Integer enthält. Ist trotzdem nicht erlaubt, und zwar aus gutem Grund (meiner Meinung nach)

Übrigens: Wenn ich vor dem Try-Block IOException ex = null; schreibe, ist es nicht erlaubt, im Catch-Block ex = e; zuzuweisen, was ich inkonsistent finde.

Ich hatte mal an einer Datenverarbeitung für die Flugzeugsteuerung geschrieben, wo es sehr sehr wichtig ist dass das Programm sicher und zuverlässig arbeitet. Dort hatte ich nach jahrelanger Erfahrung zum ersten mal richtig gelernt wie wichtig Exceptions sind, aber auch wie man sie richtig anwendet und verwendet. Unchecked Exceptions wie bei C# möchte ich mir gar nicht mehr vorstellen.
Lambdas in Java sind einfach nur eine Fehlkonstruktion, eben wegen weil dort Exceptions von Haus aus nicht weitergeleitet werden. Den Interfaces der Standardbibliothek fehlt das obligatorische „throws Throwable“, aber dann wiederum müsste man jedes Lambda in ein try-catch Block packen…

…ES SEI DENN…

Und hier kommen wir wieder auf das Thema zurück! …der Compiler erkennt dass es keine Exception gibt die gecatcht werden müsste.

Nicht unbedingt. Man könnte ein Interface haben wie

public interface ThrowingSupplier<T, E extends Exception> {
    T get() throws E;
}

Wenn man dann z.B. als Argument ThrowingSupplier<String, RuntimeException> erwartet, muss man kein try schreiben (wenn das Teil der Java API wäre, könnte man auch interface Supplier<T> extends ThrowingSupplier<X, RuntimeException> definieren). Dann hätte man ziemlich viel Flexibilität beim Definieren von Lambdas, und brauchte das try auch nur dann, wenn wirklich eine checked Exception zu erwarten ist.

Es gibt sowas wie https://github.com/pivovarit/throwing-function , aber um mehr (allgemeineres) zu dem Thema zu sagen, muss ich noch mehr Zeit allokieren.