532 Stimmen

Das Argument gegen kontrollierte Ausnahmen

Seit einigen Jahren ist es mir nicht gelungen, eine vernünftige Antwort auf die folgende Frage zu finden: Warum sind einige Entwickler so gegen geprüfte Ausnahmen? Ich habe zahlreiche Gespräche geführt, Dinge in Blogs gelesen, gelesen, was Bruce Eckel zu sagen hatte (die erste Person, die ich sah, die sich gegen sie aussprach).

Ich schreibe gerade einen neuen Code und achte sehr genau darauf, wie ich mit Ausnahmen umgehe. Ich versuche, den Standpunkt der "Wir mögen keine geprüften Ausnahmen"-Leute zu verstehen, aber ich kann ihn immer noch nicht nachvollziehen.

Jedes Gespräch, das ich führe, endet mit der gleichen Frage, die nicht beantwortet wird... lassen Sie mich das klarstellen:

Im Allgemeinen (von der Art, wie Java entwickelt wurde),

  • Error ist für Dinge gedacht, die man niemals einfangen sollte (VM hat eine Erdnussallergie und jemand hat ein Glas Erdnüsse auf ihn fallen lassen)
  • RuntimeException ist für Dinge, die der Programmierer falsch gemacht hat (Programmierer ist über das Ende eines Arrays hinausgegangen)
  • Exception (außer RuntimeException ) ist für Dinge gedacht, die sich der Kontrolle des Programmierers entziehen (die Festplatte füllt sich beim Schreiben in das Dateisystem, das Limit für Dateihandles für den Prozess wurde erreicht und Sie können keine weiteren Dateien öffnen).
  • Throwable ist einfach der übergeordnete Typ für alle Ausnahmetypen.

Ein häufiges Argument, das ich höre, ist, dass, wenn eine Ausnahme auftritt, der Entwickler nur das Programm beenden muss.

Ein weiteres Argument, das ich häufig höre, ist, dass geprüfte Ausnahmen das Refactoring von Code erschweren.

Zu dem Argument "alles, was ich tun werde, ist, das Programm zu beenden" sage ich, dass Sie, selbst wenn Sie es beenden, eine vernünftige Fehlermeldung anzeigen müssen. Wenn Sie sich mit der Behandlung von Fehlern begnügen, werden Ihre Benutzer nicht sehr erfreut sein, wenn das Programm ohne eine klare Angabe des Grundes beendet wird.

Für die Leute, die meinen, dass dies das Refactoring erschwert, bedeutet dies, dass nicht die richtige Abstraktionsebene gewählt wurde. Anstatt eine Methode zu deklarieren, die ein IOException El IOException sollte in eine Ausnahme umgewandelt werden, die dem Geschehen besser entspricht.

Ich habe kein Problem damit, Main mit catch(Exception) (oder in einigen Fällen catch(Throwable) um sicherzustellen, dass das Programm ordnungsgemäß beendet werden kann - aber ich fange immer die spezifischen Ausnahmen ab, die ich benötige. Auf diese Weise kann ich zumindest eine entsprechende Fehlermeldung ausgeben.

Die Frage, die nie beantwortet wird, ist diese:

Wenn Sie werfen RuntimeException Unterklassen anstelle von Exception Unterklassen, wie können Sie dann wissen, was Sie fangen sollen?

Wenn die Antwort "fangen" lautet Exception dann gehen Sie auch mit Programmierfehlern genauso um wie mit Systemausnahmen. Das halte ich für falsch.

Wenn Sie sich Throwable dann behandeln Sie Systemausnahmen und VM-Fehler (und Ähnliches) auf die gleiche Weise. Das halte ich für falsch.

Wenn die Antwort lautet, dass Sie nur die Ausnahmen abfangen, von denen Sie wissen, dass sie ausgelöst werden, wie können Sie dann wissen, welche Ausnahmen ausgelöst werden? Was passiert, wenn Programmierer X eine neue Ausnahme auslöst und vergessen hat, sie abzufangen? Das erscheint mir sehr gefährlich.

Ich würde sagen, dass ein Programm, das einen Stack-Trace anzeigt, falsch ist. Empfinden Leute, die keine geprüften Ausnahmen mögen, das nicht auch so?

Wenn Sie also keine geprüften Ausnahmen mögen, können Sie bitte erklären, warum nicht UND die Frage beantworten, die nicht beantwortet wird?

Ich bin nicht auf der Suche nach Ratschlägen, wann ich eines der beiden Modelle verwenden sollte, sondern nach folgenden Informationen warum Menschen erstrecken sich von RuntimeException weil sie sich nicht gerne von Exception und/oder warum sie eine Ausnahme abfangen und dann eine RuntimeException statt ihre Methode mit Überwürfen zu versehen. Ich möchte die Gründe für die Abneigung gegen geprüfte Ausnahmen verstehen.

333voto

Rhubarb Punkte 33134

Ich glaube, ich habe das gleiche Interview mit Bruce Eckel gelesen wie Sie - und es hat mich immer gestört. In der Tat wurde das Argument von dem Interviewten (wenn dies tatsächlich der Beitrag ist, von dem Sie sprechen) Anders Hejlsberg, dem MS-Genie hinter .NET und C#, vorgebracht.

http://www.artima.com/intv/handcuffs.html

Obwohl ich ein Fan von Hejlsberg und seiner Arbeit bin, habe ich dieses Argument immer als falsch empfunden. Es läuft im Grunde auf Folgendes hinaus:

"Geprüfte Ausnahmen sind schlecht, weil Programmierer sie einfach missbrauchen, indem sie sie immer abfangen und verwerfen, was dazu führt, dass Probleme versteckt und ignoriert werden, die dem Benutzer sonst angezeigt würden".

Unter "anderweitig dem Nutzer präsentiert" Ich meine, wenn Sie eine Laufzeitausnahme verwenden, wird der faule Programmierer sie einfach ignorieren (im Gegensatz zum Abfangen mit einem leeren Catch-Block) und der Benutzer wird sie sehen.

Die Zusammenfassung des Arguments ist, dass "Die Programmierer werden sie nicht richtig verwenden, und sie nicht richtig zu verwenden ist schlimmer als sie nicht zu haben. .

An diesem Argument ist etwas Wahres dran, und tatsächlich vermute ich, dass Goslings Motivation, keine Operatorüberschreibungen in Java einzuführen, auf einem ähnlichen Argument beruht - sie verwirren den Programmierer, weil sie oft missbraucht werden.

Letztendlich halte ich dies jedoch für ein Scheinargument von Hejlsberg und möglicherweise für eine nachträgliche Erklärung des Mangels und nicht für eine gut durchdachte Entscheidung.

Ich würde argumentieren, dass die übermäßige Verwendung von geprüften Ausnahmen zwar eine schlechte Sache ist und zu schlampiger Handhabung durch die Benutzer führt, dass aber die richtige Verwendung dieser Ausnahmen es dem API-Programmierer ermöglicht, dem API-Client-Programmierer große Vorteile zu bieten.

Jetzt muss der API-Programmierer darauf achten, dass er nicht überall geprüfte Ausnahmen auslöst, da diese sonst den Client-Programmierer nur verärgern. Der sehr faule Client-Programmierer wird auf catch (Exception) {} wie Hejlsberg warnt, werden alle Vorteile verloren gehen und die Hölle wird kommen. Aber unter bestimmten Umständen gibt es einfach keinen Ersatz für eine gut geprüfte Ausnahme.

Für mich ist das klassische Beispiel die API zum Öffnen von Dateien. Jede Programmiersprache in der Geschichte der Sprachen (zumindest auf Dateisystemen) hat irgendwo eine API, mit der man eine Datei öffnen kann. Und jeder Client-Programmierer, der diese API verwendet, weiß, dass er mit dem Fall umgehen muss, dass die Datei, die er zu öffnen versucht, nicht existiert. Lassen Sie es mich anders ausdrücken: Jeder Client-Programmierer, der diese API verwendet sollten wissen dass sie sich mit diesem Fall befassen müssen. Und genau da liegt der Knackpunkt: Kann der API-Programmierer ihnen helfen, indem er ihnen mitteilt, dass sie sich allein durch Kommentare damit befassen sollten, oder können sie tatsächlich darauf bestehen der Kunde damit umgehen.

In C lautet die Redewendung etwa so

  if (f = fopen("goodluckfindingthisfile")) { ... } 
  else { // file not found ...

fopen zeigt einen Fehlschlag an, indem es 0 zurückgibt, und C lässt Sie (dummerweise) 0 als einen booleschen Wert behandeln und... Im Grunde lernt man dieses Idiom und alles ist gut. Aber was ist, wenn Sie ein Anfänger sind und das Idiom nicht gelernt haben. Dann fängt man natürlich an mit

   f = fopen("goodluckfindingthisfile");
   f.read(); // BANG! 

und lernen auf die harte Tour.

Beachten Sie, dass wir hier nur über stark typisierte Sprachen sprechen: Es gibt eine klare Vorstellung davon, was eine API in einer stark typisierten Sprache ist: Es ist ein Sammelsurium von Funktionen (Methoden), die Sie mit einem klar definierten Protokoll für jede einzelne verwenden können.

Dieses klar definierte Protokoll wird in der Regel durch eine Methodensignatur festgelegt. Hier verlangt fopen die Übergabe einer Zeichenkette (oder eines char* im Falle von C). Wenn Sie ihm etwas anderes übergeben, erhalten Sie einen Kompilierfehler. Sie haben das Protokoll nicht befolgt - Sie verwenden die API nicht richtig.

In einigen (obskuren) Sprachen ist der Rückgabetyp ebenfalls Teil des Protokolls. Wenn Sie versuchen, das Äquivalent von fopen() in einigen Sprachen, ohne sie einer Variablen zuzuweisen, erhalten Sie ebenfalls einen Kompilierfehler (das ist nur bei ungültigen Funktionen möglich).

Was ich damit sagen will, ist Folgendes: In einer statisch typisierten Sprache ermutigt der API-Programmierer den Client, die API richtig zu verwenden, indem er verhindert, dass sein Client-Code kompiliert wird, wenn er offensichtliche Fehler macht.

(In einer dynamisch typisierten Sprache wie Ruby können Sie als Dateinamen irgendetwas, z. B. einen Float, übergeben - und es wird kompiliert. Warum sollte man den Benutzer mit geprüften Ausnahmen belästigen, wenn man nicht einmal die Methodenargumente kontrollieren kann. Die hier angeführten Argumente gelten nur für statisch typisierte Sprachen).

Und was ist mit den geprüften Ausnahmen?

Hier ist eine der Java-APIs, die Sie zum Öffnen einer Datei verwenden können.

try {
  f = new FileInputStream("goodluckfindingthisfile");
}
catch (FileNotFoundException e) {
  // deal with it. No really, deal with it!
  ... // this is me dealing with it
}

Sehen Sie den Haken? Hier ist die Signatur für diese API-Methode:

public FileInputStream(String name)
                throws FileNotFoundException

Beachten Sie, dass FileNotFoundException ist eine geprüft Ausnahme.

Der API-Programmierer sagt dies zu Ihnen: "Sie können diesen Konstruktor verwenden, um einen neuen FileInputStream zu erstellen, aber Sie

a) muss geben Sie den Dateinamen als Zeichenfolge
b) muss akzeptieren, dass die Möglichkeit, dass die Datei nicht zur Laufzeit nicht gefunden wird"

Und das ist für mich der springende Punkt.

Der Schlüssel ist im Grunde das, was in der Frage als "Dinge, die außerhalb der Kontrolle des Programmierers liegen" bezeichnet wird. Mein erster Gedanke war, dass er/sie Dinge meint, die außerhalb der API Kontrolle des Programmierers. Aber eigentlich sollten geprüfte Ausnahmen, wenn sie richtig eingesetzt werden, für Dinge verwendet werden, die sich sowohl der Kontrolle des Client-Programmierers als auch der des API-Programmierers entziehen. Ich denke, dies ist der Schlüssel dazu, geprüfte Ausnahmen nicht zu missbrauchen.

Ich denke, die geöffnete Datei veranschaulicht den Punkt sehr gut. Der API-Programmierer weiß, dass Sie ihm einen Dateinamen geben könnten, der sich zum Zeitpunkt des API-Aufrufs als nicht existent herausstellt, und dass er Ihnen nicht das Gewünschte zurückgeben kann, sondern eine Ausnahme auslösen muss. Sie wissen auch, dass dies ziemlich häufig vorkommt und dass der Client-Programmierer davon ausgeht, dass der Dateiname zum Zeitpunkt des Aufrufs korrekt ist, dass er aber zur Laufzeit aus Gründen, die außerhalb seiner Kontrolle liegen, falsch sein kann.

Die API macht es also explizit: Es wird Fälle geben, in denen diese Datei zu dem Zeitpunkt, zu dem Sie mich aufrufen, nicht existiert, und Sie sollten sich besser darum kümmern.

Dies wäre mit einem Gegenbeispiel deutlicher. Stellen Sie sich vor, ich schreibe eine Tabellen-API. Ich habe das Tabellenmodell irgendwo mit einer API, die diese Methode enthält:

public RowData getRowData(int row) 

Als API-Programmierer weiß ich, dass es Fälle geben wird, in denen ein Client einen negativen Wert für die Zeile oder einen Zeilenwert außerhalb der Tabelle übergibt. Daher könnte ich versucht sein, eine geprüfte Ausnahme auszulösen und den Client zu zwingen, damit umzugehen:

public RowData getRowData(int row) throws CheckedInvalidRowNumberException

(Ich würde es natürlich nicht wirklich "geprüft" nennen.)

Dies ist eine schlechte Verwendung von geprüften Ausnahmen. Der Client-Code wird voll von Aufrufen zum Abrufen von Zeilendaten sein, von denen jeder ein try/catch verwenden muss, und wofür? Sollen sie dem Benutzer mitteilen, dass die falsche Zeile gesucht wurde? Wahrscheinlich nicht - denn wie auch immer die Benutzeroberfläche meiner Tabellenansicht aussieht, sie sollte den Benutzer nicht in einen Zustand geraten lassen, in dem eine illegale Zeile angefordert wird. Es handelt sich also um einen Fehler auf Seiten des Client-Programmierers.

Der API-Programmierer kann immer noch vorhersagen, dass der Client solche Fehler codieren wird und sollte sie mit einer Laufzeitausnahme wie einer IllegalArgumentException .

Mit einer geprüften Ausnahme in getRowData ist dies eindeutig ein Fall, der dazu führen wird, dass Hejlsbergs faule Programmierer einfach leere Fänge hinzufügen. Wenn das passiert, sind die illegalen Zeilenwerte nicht einmal für den Tester oder den Client-Entwickler beim Debuggen offensichtlich, sondern führen zu Folgefehlern, deren Ursache schwer zu ermitteln ist. Arianne-Raketen werden nach dem Start explodieren.

Okay, also hier ist das Problem: Ich sage, dass die geprüfte Ausnahme FileNotFoundException ist nicht nur eine gute Sache, sondern ein unverzichtbares Werkzeug im Werkzeugkasten des API-Programmierers, um die API so zu definieren, dass sie für den Client-Programmierer möglichst nützlich ist. Aber die CheckedInvalidRowNumberException ist eine große Unannehmlichkeit, die zu schlechter Programmierung führt und vermieden werden sollte. Aber wie kann man den Unterschied erkennen?

Ich denke, es ist keine exakte Wissenschaft, und ich denke, dass das Hejlsbergs Argument zugrunde liegt und vielleicht bis zu einem gewissen Grad gerechtfertigt ist. Aber ich möchte hier nicht das Kind mit dem Bade ausschütten. Erlauben Sie mir daher, hier einige Regeln aufzustellen, um gute geprüfte Ausnahmen von schlechten zu unterscheiden:

  1. Außerhalb der Kontrolle des Kunden oder geschlossen vs. offen:

    Geprüfte Ausnahmen sollten nur verwendet werden, wenn der Fehlerfall außerhalb der Kontrolle sowohl der API et der Client-Programmierer. Dies hat damit zu tun, wie öffnen o geschlossen das System ist. In einem Zwangsweise UI, bei dem der Client-Programmierer z. B. die Kontrolle über alle Schaltflächen, Tastaturbefehle usw. hat, die Zeilen in der Tabellenansicht hinzufügen und löschen (ein geschlossenes System), ist es ein Fehler in der Client-Programmierung, wenn versucht wird, Daten aus einer nicht vorhandenen Zeile zu holen. In einem dateibasierten Betriebssystem, in dem eine beliebige Anzahl von Benutzern/Anwendungen Dateien hinzufügen und löschen können (ein offenes System), ist es denkbar, dass die Datei, die der Client anfordert, ohne sein Wissen gelöscht wurde, so dass von ihm erwartet werden sollte, dass er sich darum kümmert.

  2. Ubiquität:

    Geprüfte Ausnahmen sollten nicht bei einem API-Aufruf verwendet werden, der häufig vom Client durchgeführt wird. Mit häufig meine ich von vielen Stellen im Client-Code - nicht häufig in der Zeit. So neigt ein Client-Code nicht dazu, dieselbe Datei häufig zu öffnen, aber meine Tabellensicht erhält RowData mit verschiedenen Methoden. Insbesondere werde ich eine Menge Code schreiben wie

    if (model.getRowData().getCell(0).isEmpty())

und es wird schmerzhaft sein, jedes Mal in try/catch einpacken zu müssen.

  1. Information des Nutzers:

    Geprüfte Ausnahmen sollten in Fällen verwendet werden, in denen Sie sich vorstellen können, dass dem Endbenutzer eine nützliche Fehlermeldung angezeigt wird. Dies ist die "Und was werden Sie tun, wenn es passiert?" Frage, die ich oben gestellt habe. Sie bezieht sich auch auf Punkt 1. Da Sie vorhersagen können, dass etwas außerhalb Ihres Client-API-Systems dazu führen könnte, dass die Datei nicht vorhanden ist, können Sie den Benutzer vernünftigerweise darüber informieren:

    "Error: could not find the file 'goodluckfindingthisfile'"

    Da die illegale Zeilennummer durch einen internen Fehler und ohne Verschulden des Benutzers verursacht wurde, gibt es keine nützlichen Informationen, die Sie ihm geben können. Wenn Ihre Anwendung keine Laufzeitausnahmen an die Konsole weiterleitet, wird sie wahrscheinlich eine hässliche Meldung wie "Ich habe einen Fehler gemacht" ausgeben:

    "Internal error occured: IllegalArgumentException in ...."

    Kurz gesagt, wenn Sie nicht glauben, dass Ihr Kundenprogrammierer Ihre Ausnahme so erklären kann, dass sie dem Benutzer hilft, dann sollten Sie wahrscheinlich keine geprüfte Ausnahme verwenden.

Das sind also meine Regeln. Sie sind etwas ausgeklügelt, und es wird zweifellos Ausnahmen geben (bitte helfen Sie mir, sie zu verfeinern, wenn Sie wollen). Aber mein Hauptargument ist, dass es Fälle gibt wie FileNotFoundException wobei die geprüfte Ausnahme ein ebenso wichtiger und nützlicher Teil des API-Vertrags ist wie die Parametertypen. Wir sollten sie also nicht abschaffen, nur weil sie missbraucht wird.

Tut mir leid, ich wollte das nicht so lang und schwammig machen. Lassen Sie mich mit zwei Vorschlägen schließen:

A: API-Programmierer: Verwenden Sie geprüfte Ausnahmen sparsam, um ihre Nützlichkeit zu erhalten. Im Zweifelsfall verwenden Sie eine ungeprüfte Ausnahme.

B: Client-Programmierer: Machen Sie es sich zur Gewohnheit, schon früh in der Entwicklung eine Wrapped Exception zu erstellen (googeln Sie es). JDK 1.4 und später bieten einen Konstruktor in RuntimeException dafür, aber Sie können auch leicht Ihre eigenen erstellen. Hier ist der Konstruktor:

public RuntimeException(Throwable cause)

Dann machen Sie es sich zur Gewohnheit, immer dann, wenn Sie eine geprüfte Ausnahme behandeln müssen und Sie sich faul fühlen (oder Sie denken, dass der API-Programmierer übereifrig war, die geprüfte Ausnahme überhaupt zu verwenden), die Ausnahme nicht einfach zu schlucken, sondern sie einzuwickeln und erneut zu verwerfen.

try {
  overzealousAPI(thisArgumentWontWork);
}
catch (OverzealousCheckedException exception) {
  throw new RuntimeException(exception);  
}

Fügen Sie dies in eine der kleinen Code-Vorlagen Ihrer IDE ein und verwenden Sie es, wenn Sie faul sind. Auf diese Weise sind Sie, wenn Sie die geprüfte Ausnahme wirklich behandeln müssen, gezwungen, zurückzukommen und sich darum zu kümmern, nachdem Sie das Problem zur Laufzeit gesehen haben. Denn, glauben Sie mir (und Anders Hejlsberg), Sie werden nie wieder zu diesem TODO in Ihrem

catch (Exception e) { /* TODO deal with this at some point (yeah right) */}

65voto

Boann Punkte 46908

Ich weiß, dass dies eine alte Frage ist, aber ich habe mich eine Weile mit geprüften Ausnahmen herumgeschlagen und möchte noch etwas hinzufügen. Bitte verzeihen Sie mir die Länge der Frage!

Mein Hauptproblem mit geprüften Ausnahmen ist, dass sie die Polymorphie ruinieren. Es ist unmöglich, sie mit polymorphen Schnittstellen in Einklang zu bringen.

Nimm das gute alte Java List Schnittstelle. Wir haben gemeinsame In-Memory-Implementierungen wie ArrayList y LinkedList . Wir haben auch die Skelettklasse AbstractList was die Erstellung neuer Arten von Listen erleichtert. Für eine Nur-Lese-Liste müssen wir nur zwei Methoden implementieren: size() y get(int index) .

Dieses Beispiel WidgetList Klasse liest einige Objekte fester Größe vom Typ Widget (nicht gezeigt) aus einer Datei:

class WidgetList extends AbstractList<Widget> {
    private static final int SIZE_OF_WIDGET = 100;
    private final RandomAccessFile file;

    public WidgetList(RandomAccessFile file) {
        this.file = file;
    }

    @Override
    public int size() {
        return (int)(file.length() / SIZE_OF_WIDGET);
    }

    @Override
    public Widget get(int index) {
        file.seek((long)index * SIZE_OF_WIDGET);
        byte[] data = new byte[SIZE_OF_WIDGET];
        file.read(data);
        return new Widget(data);
    }
}

Durch die Darstellung der Widgets mit der bekannten List Schnittstelle können Sie Elemente abrufen ( list.get(123) ) oder eine Liste iterieren ( for (Widget w : list) ... ), ohne dass man etwas über WidgetList selbst. Man kann diese Liste an alle Standardmethoden übergeben, die generische Listen verwenden, oder sie in eine Collections.synchronizedList . Der Code, der ihn verwendet, muss weder wissen noch sich darum kümmern, ob die "Widgets" an Ort und Stelle erstellt werden, aus einem Array stammen oder aus einer Datei, einer Datenbank, aus dem Netz oder von einem zukünftigen Subraumrelais gelesen werden. Es wird immer noch korrekt funktionieren, weil die List Schnittstelle korrekt implementiert ist.

Aber das ist es nicht. Die obige Klasse lässt sich nicht kompilieren, weil die Dateizugriffsmethoden ein IOException eine geprüfte Ausnahme, die Sie "abfangen oder angeben" müssen. Sie kann es nicht als geworfen angeben -- der Compiler lässt das nicht zu, weil das gegen den Vertrag der Software verstoßen würde. List Schnittstelle. Und es gibt keine sinnvolle Möglichkeit, die WidgetList selbst mit der Ausnahme umgehen kann (wie ich später noch erläutern werde).

Offensichtlich ist das einzige, was zu tun ist, fangen und wieder auswerfen geprüften Ausnahmen als einige ungeprüfte Ausnahme:

@Override
public int size() {
    try {
        return (int)(file.length() / SIZE_OF_WIDGET);
    } catch (IOException e) {
        throw new WidgetListException(e);
    }
}

public static class WidgetListException extends RuntimeException {
    public WidgetListException(Throwable cause) {
        super(cause);
    }
}

((Bearbeiten: Java 8 hat eine UncheckedIOException Klasse für genau diesen Fall: zum Auffangen und Wiederauswerfen IOException s über polymorphe Methodengrenzen hinweg. Das beweist meinen Standpunkt!))

So geprüfte Ausnahmen einfach nicht funktionieren in Fällen wie diesem. Man kann sie nicht wegwerfen. Das Gleiche gilt für eine clevere Map die durch eine Datenbank oder eine Implementierung von java.util.Random über einen COM-Anschluss mit einer Quantenentropiequelle verbunden. Sobald Sie versuchen, etwas Neues mit der Implementierung einer polymorphen Schnittstelle anzufangen, scheitert das Konzept der geprüften Ausnahmen. Aber geprüfte Ausnahmen sind so heimtückisch, dass sie einen trotzdem nicht in Ruhe lassen, denn man muss sie immer noch abfangen und von Methoden auf niedrigerer Ebene wieder auslösen, was den Code und die Stack-Trace unübersichtlich macht.

Ich finde, dass die allgegenwärtige Runnable Schnittstelle wird oft in diese Ecke gedrängt, wenn sie etwas aufruft, das geprüfte Ausnahmen auslöst. Sie kann die Ausnahme nicht selbst auslösen, also kann sie nur den Code durch Abfangen und erneutes Auslösen als RuntimeException .

Eigentlich sind Sie kann nicht deklarierte geprüfte Ausnahmen auslösen, wenn Sie auf Hacks zurückgreifen. Die JVM kümmert sich zur Laufzeit nicht um geprüfte Ausnahmeregeln, also müssen wir nur den Compiler täuschen. Der einfachste Weg, dies zu tun, ist der Missbrauch von Generics. Dies ist meine Methode dafür (der Klassenname wird angezeigt, weil er (vor Java 8) in der Aufrufsyntax für die generische Methode erforderlich ist):

class Util {
    /**
     * Throws any {@link Throwable} without needing to declare it in the
     * method's {@code throws} clause.
     * 
     * <p>When calling, it is suggested to prepend this method by the
     * {@code throw} keyword. This tells the compiler about the control flow,
     * about reachable and unreachable code. (For example, you don't need to
     * specify a method return value when throwing an exception.) To support
     * this, this method has a return type of {@link RuntimeException},
     * although it never returns anything.
     * 
     * @param t the {@code Throwable} to throw
     * @return nothing; this method never returns normally
     * @throws Throwable that was provided to the method
     * @throws NullPointerException if {@code t} is {@code null}
     */
    public static RuntimeException sneakyThrow(Throwable t) {
        return Util.<RuntimeException>sneakyThrow1(t);
    }

    @SuppressWarnings("unchecked")
    private static <T extends Throwable> RuntimeException sneakyThrow1(
            Throwable t) throws T {
        throw (T)t;
    }
}

Hurra! Damit können wir eine geprüfte Ausnahme beliebig tief auf den Stapel werfen, ohne sie zu deklarieren, ohne sie in eine RuntimeException und zwar ohne den Stack-Trace zu überladen! Nochmals das Beispiel "WidgetList":

@Override
public int size() {
    try {
        return (int)(file.length() / SIZE_OF_WIDGET);
    } catch (IOException e) {
        throw sneakyThrow(e);
    }
}

Leider besteht die letzte Beleidigung der geprüften Ausnahmen darin, dass der Compiler es Ihnen verweigert fangen eine geprüfte Ausnahme, wenn sie nach ihrer fehlerhaften Meinung nicht hätte ausgelöst werden dürfen. (Für ungeprüfte Ausnahmen gilt diese Regel nicht.) Um die heimlich ausgelöste Ausnahme abzufangen, müssen wir Folgendes tun:

try {
    ...
} catch (Throwable t) { // catch everything
    if (t instanceof IOException) {
        // handle it
        ...
    } else {
        // didn't want to catch this one; let it go
        throw t;
    }
}

Das ist ein bisschen umständlich, aber auf der positiven Seite ist es immer noch etwas einfacher als der Code zum Extrahieren einer geprüften Ausnahme, die in eine RuntimeException .

Erfreulicherweise ist die throw t; Anweisung ist hier legal, auch wenn der Typ der t überprüft wird, dank einer in Java 7 hinzugefügten Regel über das erneute Auslösen von gefangenen Ausnahmen.


Wenn geprüfte Ausnahmen auf Polymorphismus treffen, ist auch der umgekehrte Fall ein Problem: wenn eine Methode so spezifiziert ist, dass sie möglicherweise eine geprüfte Ausnahme auslöst, eine überschriebene Implementierung dies aber nicht tut. Zum Beispiel kann die abstrakte Klasse OutputStream 's write Methoden spezifizieren alle throws IOException . ByteArrayOutputStream ist eine Unterklasse, die in ein speicherinternes Array schreibt, anstatt in eine echte E/A-Quelle. Ihr überschriebenes write Methoden können nicht zu IOException s, also haben sie keine throws Klausel, und Sie können sie aufrufen, ohne sich um die catch-or-specify-Anforderung zu kümmern.

Aber nicht immer. Angenommen, dass Widget verfügt über eine Methode zum Speichern in einem Stream:

public void writeTo(OutputStream out) throws IOException;

Die Deklaration dieser Methode zur Annahme einer einfachen OutputStream ist das Richtige, damit es polymorph mit allen Arten von Ausgaben verwendet werden kann: Dateien, Datenbanken, das Netzwerk und so weiter. Und In-Memory-Arrays. Bei einem In-Memory-Array gibt es jedoch eine fiktive Anforderung, eine Ausnahme zu behandeln, die eigentlich nicht auftreten kann:

ByteArrayOutputStream out = new ByteArrayOutputStream();
try {
    someWidget.writeTo(out);
} catch (IOException e) {
    // can't happen (although we shouldn't ignore it if it does)
    throw new RuntimeException(e);
}

Wie üblich kommen uns die geprüften Ausnahmen in die Quere. Wenn Ihre Variablen als Basistyp deklariert sind, der offenere Ausnahmeanforderungen hat, müssen Sie Handler für diese Ausnahmen hinzufügen, selbst wenn Sie wissen werden sie in Ihrer Anwendung nicht vorkommen.

Aber halt, geprüfte Ausnahmen sind eigentlich also ärgerlich, dass Sie lassen dich nicht einmal den umgekehrten Weg gehen! Stellen Sie sich vor, Sie fangen gerade eine IOException geworfen von write Anrufe bei einem OutputStream aber Sie möchten den deklarierten Typ der Variablen in einen ByteArrayOutputStream wird der Compiler Sie beschimpfen, wenn Sie versuchen, eine geprüfte Ausnahme abzufangen, die laut Compiler nicht geworfen werden darf.

Diese Regel verursacht einige absurde Probleme. Zum Beispiel, eine der drei write Methoden der OutputStream es pas außer Kraft gesetzt durch ByteArrayOutputStream . Konkret, write(byte[] data) ist eine einfache Methode, die das gesamte Array durch den Aufruf von write(byte[] data, int offset, int length) mit einem Offset von 0 und der Länge des Arrays. ByteArrayOutputStream überschreibt die Drei-Argument-Methode, erbt aber die Ein-Argument-Komfort-Methode so wie sie ist. Die geerbte Methode tut genau das Richtige, aber sie enthält eine unerwünschte throws Klausel. Das war vielleicht ein Versehen bei der Gestaltung von ByteArrayOutputStream aber sie können es nie beheben, weil es die Quellcode-Kompatibilität mit jedem Code, der die Ausnahme abfängt, brechen würde - die Ausnahme, die nie ausgelöst wurde, nie ausgelöst wird und nie ausgelöst werden wird!

Diese Regel ist auch beim Bearbeiten und Debuggen lästig. Manchmal kommentiere ich z.B. einen Methodenaufruf vorübergehend aus, und wenn er eine geprüfte Ausnahme hätte auslösen können, beschwert sich der Compiler jetzt über die Existenz der lokalen try y catch Blöcke. Also muss ich auch diese auskommentieren, und wenn ich nun den Code darin bearbeite, rückt die IDE auf die falsche Ebene ein, weil die { y } sind auskommentiert. Auweia! Es ist eine kleine Beschwerde, aber es scheint, als ob das einzige, was geprüfte Ausnahmen jemals tun, Probleme verursacht.


Ich bin fast fertig. Meine letzte Frustration mit geprüften Ausnahmen ist, dass an den meisten Anrufstellen können Sie nichts Nützliches mit ihnen anfangen. Im Idealfall haben wir, wenn etwas schief geht, einen kompetenten anwendungsspezifischen Handler, der den Benutzer über das Problem informiert und/oder den Vorgang beendet oder erneut versucht. Nur ein Handler weiter oben im Stapel kann dies tun, weil er als einziger das Gesamtziel kennt.

Stattdessen gibt es die folgende Redewendung, die weit verbreitet ist, um den Compiler zum Schweigen zu bringen:

try {
    ...
} catch (SomeStupidExceptionOmgWhoCares e) {
    e.printStackTrace();
}

In einer grafischen Benutzeroberfläche oder einem automatisierten Programm wird die gedruckte Meldung nicht angezeigt. Schlimmer noch, der Rest des Codes wird nach der Ausnahme fortgesetzt. Ist die Ausnahme nicht wirklich ein Fehler? Dann drucken Sie sie nicht aus. Andernfalls wird in einem Moment etwas anderes explodieren, und dann ist das ursprüngliche Ausnahmeobjekt verschwunden. Dieses Idiom ist nicht besser als BASICs On Error Resume Next oder PHPs error_reporting(0); .

Der Aufruf einer Art Logger-Klasse ist nicht viel besser:

try {
    ...
} catch (SomethingWeird e) {
    logger.log(e);
}

Das ist genau so faul wie e.printStackTrace(); und macht immer noch mit Code in einem unbestimmten Zustand weiter. Außerdem ist die Wahl eines bestimmten Protokollierungssystems oder eines anderen Handlers anwendungsspezifisch, was der Wiederverwendung von Code schadet.

Aber halt! Es gibt einen einfachen und universellen Weg, den anwendungsspezifischen Handler zu finden. Er befindet sich weiter oben auf dem Aufrufstapel (oder er ist als Threads Behandler für nicht erwischte Ausnahmen ). Also an den meisten Orten, Sie müssen die Ausnahme nur weiter oben auf dem Stapel auslösen. . Z.B., throw e; . Überprüfte Ausnahmen sind einfach nur hinderlich.

Ich bin sicher, dass geprüfte Ausnahmen eine gute Idee waren, als die Sprache entwickelt wurde, aber in der Praxis habe ich festgestellt, dass sie nur Ärger und keinen Nutzen bringen.

48voto

Richard Levasseur Punkte 13763

Nun, es geht nicht darum, einen Stacktrace anzuzeigen oder stillschweigend abzustürzen. Es geht um die Möglichkeit, Fehler zwischen den Schichten zu kommunizieren.

Das Problem mit geprüften Ausnahmen ist, dass sie die Leute dazu verleiten, wichtige Details zu verschlucken (nämlich die Ausnahmeklasse). Wenn man sich entscheidet, dieses Detail nicht zu vernachlässigen, dann muss man in der gesamten Anwendung immer wieder throws-Deklarationen hinzufügen. Das bedeutet, dass 1) ein neuer Ausnahmetyp viele Funktionssignaturen betrifft und 2) Sie eine bestimmte Instanz der Ausnahme, die Sie eigentlich abfangen wollen, übersehen können (z. B. öffnen Sie eine Sekundärdatei für eine Funktion, die Daten in eine Datei schreibt. Die sekundäre Datei ist optional, also können Sie ihre Fehler ignorieren, aber weil die Signatur throws IOException wird dies leicht übersehen).

Ich bin gerade mit dieser Situation in einer Anwendung konfrontiert. Wir haben fast alle Ausnahmen als AppSpecificException neu verpackt. Dadurch wurden die Signaturen wirklich sauber und wir mussten uns keine Sorgen über explodierende throws in Unterschriften.

Natürlich müssen wir jetzt die Fehlerbehandlung auf den höheren Ebenen spezialisieren und eine Wiederholungslogik und ähnliches implementieren. Allerdings ist alles AppSpecificException, so dass wir nicht sagen können: "Wenn eine IOException ausgelöst wird, versuche es erneut" oder "Wenn ClassNotFound ausgelöst wird, breche komplett ab". Wir haben keinen zuverlässigen Weg, um an die real Ausnahme, weil Dinge immer wieder neu verpackt werden, wenn sie zwischen unserem Code und dem Code von Drittanbietern übertragen werden.

Aus diesem Grund bin ich ein großer Fan der Ausnahmebehandlung in Python. Sie können nur die Dinge abfangen, die Sie wollen und/oder behandeln können. Alles andere blubbert auf, als ob Sie es selbst zurückgeworfen haben (was Sie sowieso getan haben).

Ich habe immer wieder festgestellt, dass die Behandlung von Ausnahmen im Rahmen des erwähnten Projekts in 3 Kategorien fällt:

  1. Fangen und behandeln einer spezifisch Ausnahme. Dies dient zum Beispiel der Implementierung einer Wiederholungslogik.
  2. Fangen und Weiterwerfen andere Ausnahmen. Alles, was hier passiert, ist in der Regel eine Protokollierung, und zwar in der Regel eine banale Meldung wie "Unable to open $filename". Das sind Fehler, gegen die man nichts machen kann; nur eine höhere Ebene weiß genug, um sie zu behandeln.
  3. Fangen Sie alles ab und geben Sie eine Fehlermeldung aus. Dies ist in der Regel an der Wurzel eines Dispatchers, und alles, was es tut, ist sicherzustellen, dass es den Fehler an den Aufrufer über einen Nicht-Ausnahme-Mechanismus (Popup-Dialog, marshaling ein RPC-Fehler-Objekt, etc.) kommunizieren kann.

25voto

Luke Quinane Punkte 16188

SNR

Erstens verringern geprüfte Ausnahmen das "Signal-Rausch-Verhältnis" für den Code. Anders Hejlsberg spricht auch über imperative vs. deklarative Programmierung, was ein ähnliches Konzept ist. Wie auch immer, betrachten Sie die folgenden Codeschnipsel:

Aktualisierung der Benutzeroberfläche von einem Nicht-UI-Thread in Java:

try {  
    // Run the update code on the Swing thread  
    SwingUtilities.invokeAndWait(() -> {  
        try {
            // Update UI value from the file system data  
            FileUtility f = new FileUtility();  
            uiComponent.setValue(f.readSomething());
        } catch (IOException e) {  
            throw new UncheckedIOException(e);
        }
    });
} catch (InterruptedException ex) {  
    throw new IllegalStateException("Interrupted updating UI", ex);  
} catch (InvocationTargetException ex) {
    throw new IllegalStateException("Invocation target exception updating UI", ex);
}

Update UI von nicht UI-Thread in C#:

private void UpdateValue()  
{  
   // Ensure the update happens on the UI thread  
   if (InvokeRequired)  
   {  
       Invoke(new MethodInvoker(UpdateValue));  
   }  
   else  
   {  
       // Update UI value from the file system data  
       FileUtility f = new FileUtility();  
       uiComponent.Value = f.ReadSomething();  
   }  
}  

Das scheint mir viel klarer zu sein. Wenn Sie beginnen, mehr und mehr UI Arbeit in Swing überprüfen Ausnahmen beginnen, wirklich ärgerlich und nutzlos zu werden.

Ausbruch aus dem Gefängnis

Um selbst die einfachsten Implementierungen wie die Java-Schnittstelle "List" zu implementieren, sind geprüfte Ausnahmen als Hilfsmittel für den Entwurf nach Vertrag nicht geeignet. Nehmen wir eine Liste, die von einer Datenbank oder einem Dateisystem oder einer anderen Implementierung unterstützt wird, die eine geprüfte Ausnahme auslöst. Die einzig mögliche Implementierung besteht darin, die geprüfte Ausnahme abzufangen und sie als ungeprüfte Ausnahme erneut zu werfen:

@Override
public void clear()  
{  
   try  
   {  
       backingImplementation.clear();  
   }  
   catch (CheckedBackingImplException ex)  
   {  
       throw new IllegalStateException("Error clearing underlying list.", ex);  
   }  
}  

Jetzt stellt sich die Frage: Was ist der Sinn dieses ganzen Codes? Die geprüften Ausnahmen fügen nur Lärm hinzu, die Ausnahme wurde zwar abgefangen, aber nicht behandelt, und der vertragsgemäße Entwurf (in Bezug auf die geprüften Ausnahmen) ist gescheitert.

Schlussfolgerung

  • Das Auffangen von Ausnahmen ist etwas anderes als deren Behandlung.
  • Geprüfte Ausnahmen bringen Unruhe in den Code.
  • Die Behandlung von Ausnahmen funktioniert in C# auch ohne sie.

Ich habe darüber gebloggt zuvor .

23voto

Esko Luontola Punkte 71758

Der Artikel Wirksame Java-Ausnahmen erklärt sehr schön, wann ungeprüfte und wann geprüfte Ausnahmen verwendet werden sollten. Hier sind einige Zitate aus diesem Artikel, um die wichtigsten Punkte hervorzuheben:

Unvorhergesehenes: Eine erwartete Bedingung, die eine alternative Antwort von einer Methode verlangt, die in Bezug auf den beabsichtigten Zweck der Methode ausgedrückt werden kann. Der Aufrufer der Methode erwartet diese Art von Bedingungen und hat eine Strategie, um mit ihnen umzugehen.

Störung: Ein ungeplanter Zustand, der eine Methode daran hindert, ihren beabsichtigten Zweck zu erfüllen, und der nicht ohne Bezugnahme auf die interne Implementierung der Methode beschrieben werden kann.

(SO erlaubt keine Tabellen, daher sollten Sie vielleicht das Folgende aus der Originalseite ...)

Kontingente

  • Wird als solche betrachtet: Ein Teil des Musters
  • Es wird erwartet, dass dies geschieht: Regelmäßig, aber selten
  • Wen interessiert das schon: Der vorgelagerte Code, der die Methode aufruft
  • Beispiele: Alternative Rückführungsmodi
  • Beste Kartierung: Eine kontrollierte Ausnahme

Störung

  • Gilt als: Eine böse Überraschung
  • Es wird erwartet, dass dies geschieht: Niemals
  • Wen interessiert das schon: Die Menschen, die das Problem lösen müssen
  • Beispiele: Programmierfehler, Fehlfunktionen der Hardware, Konfigurationsfehler, fehlende Dateien, nicht verfügbare Server
  • Bestes Mapping: Eine ungeprüfte Ausnahme

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X