Sind geprüfte Ausnahmen gut oder schlecht?

Java unterstützt geprüfte Ausnahmen. Diese kontroverse Sprachfunktion wird von einigen geliebt und von anderen gehasst, bis zu dem Punkt, an dem die meisten Programmiersprachen geprüfte Ausnahmen vermeiden und nur ihre ungeprüften Gegenstücke unterstützen.

In diesem Beitrag untersuche ich die Kontroverse um geprüfte Ausnahmen. Ich stelle zunächst das Ausnahmekonzept vor und beschreibe kurz die Sprachunterstützung von Java für Ausnahmen, um Anfängern ein besseres Verständnis der Kontroverse zu ermöglichen.

Was sind Ausnahmen?

In einer idealen Welt würden Computerprogramme niemals auf Probleme stoßen: Dateien würden existieren, wenn sie existieren sollen, Netzwerkverbindungen würden niemals unerwartet schließen, es würde niemals versucht werden, eine Methode über die Nullreferenz Integer-Division-by aufzurufen -Null-Versuche würden nicht auftreten, und so weiter. Unsere Welt ist jedoch alles andere als ideal; Diese und andere Ausnahmen von der idealen Programmausführung sind weit verbreitet.

Frühe Versuche, Ausnahmen zu erkennen, umfassten die Rückgabe spezieller Werte, die auf einen Fehler hinweisen. Beispielsweise wird die fopen()Funktion der C-Sprache zurückgegeben , NULLwenn eine Datei nicht geöffnet werden kann. Außerdem gibt die PHP- mysql_query()Funktion zurück, FALSEwenn ein SQL-Fehler auftritt. Sie müssen an anderer Stelle nach dem tatsächlichen Fehlercode suchen. Obwohl es einfach zu implementieren ist, gibt es zwei Probleme mit diesem Ansatz "Sonderwert zurückgeben" beim Erkennen von Ausnahmen:

  • Spezielle Werte beschreiben die Ausnahme nicht. Was bedeutet NULLoder bedeutet das FALSEwirklich? Es hängt alles vom Autor der Funktionalität ab, die den speziellen Wert zurückgibt. Wie verknüpfen Sie einen speziellen Wert mit dem Kontext des Programms, als die Ausnahme auftrat, damit Sie dem Benutzer eine aussagekräftige Nachricht präsentieren können?
  • Es ist zu einfach, einen bestimmten Wert zu ignorieren. Zum Beispiel int c; FILE *fp = fopen("data.txt", "r"); c = fgetc(fp);ist problematisch , da dieses C - Codefragment führt fgetc()ein Zeichen aus der Datei selbst dann , wenn lesen fopen()zurückkehrt NULL. In diesem Fall fgetc()wird es nicht gelingen: Wir haben einen Fehler, der möglicherweise schwer zu finden ist.

Das erste Problem wird durch die Verwendung von Klassen zur Beschreibung von Ausnahmen gelöst. Der Name einer Klasse identifiziert die Art der Ausnahme und ihre Felder aggregieren den geeigneten Programmkontext, um (über Methodenaufrufe) zu bestimmen, was schief gelaufen ist. Das zweite Problem wird gelöst, indem der Compiler den Programmierer zwingt, entweder direkt auf eine Ausnahme zu antworten oder anzugeben, dass die Ausnahme an anderer Stelle behandelt werden soll.

Einige Ausnahmen sind sehr ernst. Beispielsweise könnte ein Programm versuchen, Speicher zuzuweisen, wenn kein freier Speicher verfügbar ist. Ein weiteres Beispiel ist die grenzenlose Rekursion, die den Stapel erschöpft. Solche Ausnahmen werden als Fehler bezeichnet .

Ausnahmen und Java

Java verwendet Klassen, um Ausnahmen und Fehler zu beschreiben. Diese Klassen sind in einer Hierarchie organisiert, die in der java.lang.ThrowableKlasse verwurzelt ist . (Der Grund, warum Throwablediese spezielle Klasse benannt wurde, wird in Kürze ersichtlich.) Direkt darunter Throwablebefinden sich die Klassen java.lang.Exceptionund java.lang.Error, die Ausnahmen bzw. Fehler beschreiben.

Zum Beispiel enthält die Java-Bibliothek java.net.URISyntaxException, die erweitert Exceptionund angibt, dass eine Zeichenfolge nicht als Referenz für eine einheitliche Ressourcen-ID analysiert werden konnte. Beachten Sie, dass URISyntaxExceptioneine Namenskonvention folgt, bei der ein Ausnahmeklassenname mit dem Wort endet Exception. Eine ähnliche Konvention gilt für Fehlerklassennamen wie z java.lang.OutOfMemoryError.

Exceptionwird von java.lang.RuntimeExceptionklassifiziert. Dies ist die Oberklasse der Ausnahmen, die während des normalen Betriebs der Java Virtual Machine (JVM) ausgelöst werden können. Zum Beispiel java.lang.ArithmeticExceptionbeschreibt arithmetische Fehler wie Versuche ganze Zahlen zu teilen durch ganzzahlige 0. Auch java.lang.NullPointerExceptionVersuche, Zugang Objektelemente über die Null - Referenz beschreibt.

Eine andere Sichtweise RuntimeException

In Abschnitt 11.1.1 der Java 8-Sprachspezifikation heißt es: RuntimeExceptionist die Oberklasse aller Ausnahmen, die aus vielen Gründen während der Ausdrucksauswertung ausgelöst werden können, von denen jedoch möglicherweise noch eine Wiederherstellung möglich ist.

Wenn eine Ausnahme oder ein Fehler auftritt, wird ein Objekt aus der entsprechenden Exceptionoder ErrorUnterklasse erstellt und an die JVM übergeben. Das Übergeben des Objekts wird als Auslösen der Ausnahme bezeichnet . Java liefert die throwAnweisung für diesen Zweck. Erstellt beispielsweise throw new IOException("unable to read file");ein neues java.io.IOExceptionObjekt, das mit dem angegebenen Text initialisiert wird. Dieses Objekt wird anschließend an die JVM übergeben.

Java bietet die tryAnweisung zum Abgrenzen von Code, aus dem eine Ausnahme ausgelöst werden kann. Diese Anweisung besteht aus einem Schlüsselwort, trygefolgt von einem durch Klammern getrennten Block. Das folgende Codefragment demonstriert tryund throw:

try { method(); } // ... void method() { throw new NullPointerException("some text"); }

In diesem Codefragment tritt die Ausführung in den tryBlock ein und ruft auf method(), wodurch eine Instanz von ausgelöst wird NullPointerException.

Die JVM empfängt das Throwable und durchsucht den Methodenaufrufstapel nach einem Handler , der die Ausnahme behandelt. Ausnahmen, von denen nicht abgeleitet wurde, RuntimeExceptionwerden häufig behandelt. Laufzeitausnahmen und Fehler werden selten behandelt.

Warum Fehler selten behandelt werden

Fehler werden selten behandelt, da ein Java-Programm häufig nichts tun kann, um den Fehler zu beheben. Wenn beispielsweise der freie Speicher erschöpft ist, kann ein Programm keinen zusätzlichen Speicher zuweisen. Wenn der Zuordnungsfehler jedoch darauf zurückzuführen ist, dass eine Menge Speicher freigegeben wird, der freigegeben werden soll, kann ein Handler versuchen, den Speicher mithilfe der JVM freizugeben. Obwohl ein Handler in diesem Fehlerkontext nützlich zu sein scheint, ist der Versuch möglicherweise nicht erfolgreich.

Ein Handler wird durch einen catchBlock beschrieben, der dem tryBlock folgt . Der catchBlock enthält einen Header, in dem die Arten von Ausnahmen aufgeführt sind, für die er vorbereitet ist. Wenn der Typ des Throwables in der Liste enthalten ist, wird der Throwable an den catchBlock übergeben, dessen Code ausgeführt wird. Der Code reagiert auf die Fehlerursache so, dass das Programm fortgesetzt oder möglicherweise beendet wird:

try { method(); } catch (NullPointerException npe) { System.out.println("attempt to access object member via null reference"); } // ... void method() { throw new NullPointerException("some text"); }

In diesem Codefragment habe ich einen catchBlock an den tryBlock angehängt . Wenn das NullPointerExceptionObjekt von geworfen wird method(), sucht die JVM die Ausführung und übergibt sie an den catchBlock, der eine Nachricht ausgibt.

Schließlich blockiert

Auf einen tryBlock oder seinen letzten catchBlock kann ein finallyBlock folgen , der zum Ausführen von Bereinigungsaufgaben verwendet wird, z. B. zum Freigeben erworbener Ressourcen. Ich habe nichts mehr zu sagen, finallyweil es für die Diskussion nicht relevant ist.

Ausnahmen, die von Exceptionund seinen Unterklassen mit Ausnahme von RuntimeExceptionund seinen Unterklassen beschrieben werden, werden als geprüfte Ausnahmen bezeichnet . Für jede throwAnweisung überprüft der Compiler den Typ des Ausnahmeobjekts. Wenn der Typ "aktiviert" angibt, überprüft der Compiler den Quellcode, um sicherzustellen, dass die Ausnahme in der Methode behandelt wird, in der sie ausgelöst wird, oder dass sie weiter oben im Methodenaufrufstapel als behandelt deklariert wird. Alle anderen Ausnahmen werden als ungeprüfte Ausnahmen bezeichnet .

Mit Java können Sie deklarieren, dass eine geprüfte Ausnahme weiter oben im Methodenaufrufstapel behandelt wird, indem Sie eine throwsKlausel (Schlüsselwort throwsgefolgt von einer durch Kommas getrennten Liste geprüfter Ausnahmeklassennamen) an einen Methodenheader anhängen:

try { method(); } catch (IOException ioe) { System.out.println("I/O failure"); } // ... void method() throws IOException { throw new IOException("some text"); }

Da IOExceptiones sich um einen aktivierten Ausnahmetyp handelt, müssen ausgelöste Instanzen dieser Ausnahme in der Methode behandelt werden, in der sie ausgelöst werden, oder als weiter oben im Methodenaufrufstapel deklariert deklariert werden, indem eine throwsKlausel an den Header jeder betroffenen Methode angehängt wird. In diesem Fall wird eine throws IOExceptionKlausel an den method()Header angehängt . Das ausgelöste IOExceptionObjekt wird an die JVM übergeben, die die Ausführung findet und an den catchHandler überträgt .

Für und gegen geprüfte Ausnahmen argumentieren

Checked exceptions have proven to be very controversial. Are they a good language feature or are they bad? In this section, I present the cases for and against checked exceptions.

Checked exceptions are good

James Gosling created the Java language. He included checked exceptions to encourage the creation of more robust software. In a 2003 conversation with Bill Venners, Gosling pointed out how easy it is to generate buggy code in the C language by ignoring the special values that are returned from C's file-oriented functions. For example, a program attempts to read from a file that wasn't successfully opened for reading.

The seriousness of not checking return values

Not checking return values might seem like no big deal, but this sloppiness can have life-or-death consequences. For example, think about such buggy software controlling missile guidance systems and driverless cars.

Gosling also pointed out that college programming courses don't adequately discuss error handling (although that may have changed since 2003). When you go through college and you're doing assignments, they just ask you to code up the one true path [of execution where failure isn't a consideration]. I certainly never experienced a college course where error handling was at all discussed. You come out of college and the only stuff you've had to deal with is the one true path.

Focusing only on the one true path, laziness, or another factor has resulted in a lot of buggy code being written. Checked exceptions require the programmer to consider the source code's design and hopefully achieve more robust software.

Checked exceptions are bad

Many programmers hate checked exceptions because they're forced to deal with APIs that overuse them or incorrectly specify checked exceptions instead of unchecked exceptions as part of their contracts. For example, a method that sets a sensor's value is passed an invalid number and throws a checked exception instead of an instance of the unchecked java.lang.IllegalArgumentException class.

Here are a few other reasons for disliking checked exceptions; I've excerpted them from Slashdot's Interviews: Ask James Gosling About Java and Ocean Exploring Robots discussion:

  • Checked exceptions are easy to ignore by rethrowing them as RuntimeException instances, so what's the point of having them? I've lost count of the number of times I've written this block of code:
    try { // do stuff } catch (AnnoyingcheckedException e) { throw new RuntimeException(e); }

    99% of the time I can't do anything about it. Finally blocks do any necessary cleanup (or at least they should).

  • Checked exceptions can be ignored by swallowing them, so what's the point of having them? I've also lost count of the number of times I've seen this:
    try { // do stuff } catch (AnnoyingCheckedException e) { // do nothing }

    Why? Because someone had to deal with it and was lazy. Was it wrong? Sure. Does it happen? Absolutely. What if this were an unchecked exception instead? The app would've just died (which is preferable to swallowing an exception).

  • Checked exceptions result in multiple throws clause declarations. The problem with checked exceptions is they encourage people to swallow important details (namely, the exception class). If you choose not to swallow that detail, then you have to keep adding throws declarations across your whole app. This means 1) that a new exception type will affect lots of function signatures, and 2) you can miss a specific instance of the exception you actually -want- to catch (say you open a secondary file for a function that writes data to a file. The secondary file is optional, so you can ignore its errors, but because the signature throws IOException, it's easy to overlook this).
  • Checked exceptions are not really exceptions. The thing about checked exceptions is that they are not really exceptions by the usual understanding of the concept. Instead, they are API alternative return values.

    The whole idea of exceptions is that an error thrown somewhere way down the call chain can bubble up and be handled by code somewhere further up, without the intervening code having to worry about it. Checked exceptions, on the other hand, require every level of code between the thrower and the catcher to declare they know about all forms of exception that can go through them. This is really little different in practice to if checked exceptions were simply special return values which the caller had to check for.

Außerdem bin ich auf das Argument gestoßen, dass Anwendungen eine große Anzahl geprüfter Ausnahmen verarbeiten müssen, die aus den mehreren Bibliotheken generiert werden, auf die sie zugreifen. Dieses Problem kann jedoch durch eine clever gestaltete Fassade überwunden werden, die die verkettete Ausnahmefunktion von Java und das erneute Auslösen von Ausnahmen nutzt, um die Anzahl der Ausnahmen, die behandelt werden müssen, erheblich zu reduzieren und gleichzeitig die ursprüngliche Ausnahme beizubehalten, die ausgelöst wurde.

Fazit

Sind geprüfte Ausnahmen gut oder schlecht? Mit anderen Worten, sollten Programmierer gezwungen sein, geprüfte Ausnahmen zu behandeln, oder die Möglichkeit erhalten, sie zu ignorieren? Ich mag die Idee, robustere Software durchzusetzen. Ich denke jedoch auch, dass sich der Ausnahmebehandlungsmechanismus von Java weiterentwickeln muss, um programmiererfreundlicher zu werden. Hier sind einige Möglichkeiten, um diesen Mechanismus zu verbessern: