Ist das Auslösen neuer RuntimeExceptions in nicht erreichbarem Code ein schlechter Stil?

31

Ich wurde beauftragt, eine Anwendung zu warten, die vor einiger Zeit von erfahreneren Entwicklern geschrieben wurde. Ich bin auf diesen Code gestoßen:

public Configuration retrieveUserMailConfiguration(Long id) throws MailException {
        try {
            return translate(mailManagementService.retrieveUserMailConfiguration(id));
        } catch (Exception e) {
            rethrow(e);
        }
        throw new RuntimeException("cannot reach here");
    }

Ich bin gespannt, ob das Werfen RuntimeException("cannot reach here")gerechtfertigt ist. Mir fehlt wahrscheinlich etwas Offensichtliches, da ich weiß, dass dieser Code von einem erfahreneren Kollegen stammt.
EDIT: Hier ist ein Rethrow-Body, auf den sich einige Antworten beziehen. Ich hielt es für unwichtig in dieser Frage.

private void rethrow(Exception e) throws MailException {
        if (e instanceof InvalidDataException) {
            InvalidDataException ex = (InvalidDataException) e;
            rethrow(ex);
        }
        if (e instanceof EntityAlreadyExistsException) {
            EntityAlreadyExistsException ex = (EntityAlreadyExistsException) e;
            rethrow(ex);
        }
        if (e instanceof EntityNotFoundException) {
            EntityNotFoundException ex = (EntityNotFoundException) e;
            rethrow(ex);
        }
        if (e instanceof NoPermissionException) {
            NoPermissionException ex = (NoPermissionException) e;
            rethrow(ex);
        }
        if (e instanceof ServiceUnavailableException) {
            ServiceUnavailableException ex = (ServiceUnavailableException) e;
            rethrow(ex);
        }
        LOG.error("internal error, original exception", e);
        throw new MailUnexpectedException();
    }


private void rethrow(ServiceUnavailableException e) throws
            MailServiceUnavailableException {
        throw new MailServiceUnavailableException();
    }

private void rethrow(NoPermissionException e) throws PersonNotAuthorizedException {
    throw new PersonNotAuthorizedException();
}

private void rethrow(InvalidDataException e) throws
        MailInvalidIdException, MailLoginNotAvailableException,
        MailInvalidLoginException, MailInvalidPasswordException,
        MailInvalidEmailException {
    switch (e.getDetail()) {
        case ID_INVALID:
            throw new MailInvalidIdException();
        case LOGIN_INVALID:
            throw new MailInvalidLoginException();
        case LOGIN_NOT_ALLOWED:
            throw new MailLoginNotAvailableException();
        case PASSWORD_INVALID:
            throw new MailInvalidPasswordException();
        case EMAIL_INVALID:
            throw new MailInvalidEmailException();
    }
}

private void rethrow(EntityAlreadyExistsException e)
        throws MailLoginNotAvailableException, MailEmailAddressAlreadyForwardedToException {
    switch (e.getDetail()) {
        case LOGIN_ALREADY_TAKEN:
            throw new MailLoginNotAvailableException();
        case EMAIL_ADDRESS_ALREADY_FORWARDED_TO:
            throw new MailEmailAddressAlreadyForwardedToException();
    }
}

private void rethrow(EntityNotFoundException e) throws
        MailAccountNotCreatedException,
        MailAliasNotCreatedException {
    switch (e.getDetail()) {
        case ACCOUNT_NOT_FOUND:
            throw new MailAccountNotCreatedException();
        case ALIAS_NOT_FOUND:
            throw new MailAliasNotCreatedException();
    }
}
Sok Pomaranczowy
quelle
12
es ist technisch erreichbar, wenn rethroweigentlich throweine ausnahme ausfällt . (was eines Tages passieren kann, wenn sich die Implementierung ändert)
njzk2
34
Abgesehen davon ist ein AssertionError möglicherweise eine semantisch bessere Wahl als eine RuntimeException.
JvR
1
Der letzte Wurf ist überflüssig. Wenn Sie Findbugs oder andere statische Analysetools aktivieren, werden diese Linienarten zum Entfernen markiert.
Bon Ami
7
Nun , da ich den Rest des Codes zu sehen, ich denke , dass sollten Sie vielleicht „mehr qualifizierte Entwickler“ in „mehr ändern Senior Entwickler“. Code Review erklärt Ihnen gerne, warum.
JvR
3
Ich habe versucht, hypothetische Beispiele dafür zu erstellen, warum Ausnahmen schrecklich sind. Aber nichts, was ich jemals getan habe, hat damit zu tun.
Paul Draper

Antworten:

36

Zunächst einmal vielen Dank, dass Sie Ihre Frage beantwortet und uns gezeigt haben, was zu tun rethrowist. Tatsächlich konvertiert es also Ausnahmen mit Eigenschaften in feinkörnigere Klassen von Ausnahmen. Dazu später mehr.

Da ich die Hauptfrage ursprünglich nicht wirklich beantwortet habe, heißt es hier: Ja, es ist im Allgemeinen ein schlechter Stil, Laufzeitausnahmen in nicht erreichbarem Code auszulösen. Verwenden Sie besser Behauptungen oder vermeiden Sie das Problem. Wie bereits erwähnt, kann der Compiler hier nicht sicher sein, dass der Code niemals den try/catchBlock verlässt. Sie können Ihren Code umgestalten, indem Sie den Vorteil nutzen, dass ...

Fehler sind Werte

( Es ist nicht überraschend, dass es in go bekannt ist )

Nehmen wir ein einfacheres Beispiel, das ich vor Ihrer Bearbeitung verwendet habe: Stellen Sie sich vor, Sie protokollieren etwas und erstellen eine Wrapper-Ausnahme wie in Konrads Antwort . Nennen wir es logAndWrap.

Anstatt die Ausnahme als Nebeneffekt von logAndWrapauszulösen, können Sie sie als Nebeneffekt wirken lassen und eine Ausnahme zurückgeben (zumindest die in der Eingabe angegebene). Sie müssen keine Generika verwenden, sondern nur Grundfunktionen:

private Exception logAndWrap(Exception exception) {
    // or whatever it actually does
    Log.e("Ouch! " + exception.getMessage());
    return new CustomWrapperException(exception);
}

Dann throwexplizit Sie und Ihr Compiler freut sich:

try {
     return translate(mailManagementService.retrieveUserMailConfiguration(id));
} catch (Exception e) {
     throw logAndWrap(e);
}

Was ist, wenn Sie es vergessen throw?

Wie in Joe23s Kommentar erläutert , besteht eine Möglichkeit zur defensiven Programmierung , um sicherzustellen, dass die Ausnahme immer ausgelöst wird, darin, explizit throw new CustomWrapperException(exception)am Ende von ein zu machen logAndWrap, wie es von Guava.Throwables getan wird . Auf diese Weise wissen Sie, dass die Ausnahme ausgelöst wird, und Ihr Typanalysator ist glücklich. Ihre benutzerdefinierten Ausnahmen müssen jedoch deaktiviert sein, was nicht immer möglich ist. Außerdem würde ich das Risiko, dass ein throwEntwickler zum Schreiben fehlt, als sehr gering einstufen : Der Entwickler muss es vergessen und die umgebende Methode sollte nichts zurückgeben, da der Compiler sonst einen fehlenden Return feststellen würde. Dies ist eine interessante Möglichkeit, das Typensystem zu bekämpfen, und sie funktioniert trotzdem.

Erneut werfen

Das eigentliche rethrowkann auch als Funktion geschrieben werden, aber ich habe Probleme mit der aktuellen Implementierung:

  • Es gibt viele nutzlose Darsteller wie Casts sind in der Tat erforderlich (siehe Kommentare):

    if (e instanceof ServiceUnavailableException) {
        ServiceUnavailableException ex = (ServiceUnavailableException) e;
        rethrow(ex);
    }
  • Wenn neue Ausnahmen geworfen / zurückgegeben werden, wird die alte verworfen. Im folgenden Code kann MailLoginNotAvailableExceptionich mit a nicht erkennen, welche Anmeldung nicht verfügbar ist, was unpraktisch ist. Darüber hinaus sind Stacktraces unvollständig:

    private void rethrow(EntityAlreadyExistsException e)
        throws MailLoginNotAvailableException, MailEmailAddressAlreadyForwardedToException {
        switch (e.getDetail()) {
            case LOGIN_ALREADY_TAKEN:
                throw new MailLoginNotAvailableException();
            case EMAIL_ADDRESS_ALREADY_FORWARDED_TO:
                throw new MailEmailAddressAlreadyForwardedToException();
        }
    }
  • Warum löst der Ursprungscode diese speziellen Ausnahmen überhaupt nicht aus? Ich vermute, dass dies rethrowals Kompatibilitätsebene zwischen einem (Mailing-) Subsystem und der Geschäftslogik verwendet wird (möglicherweise besteht die Absicht darin, Implementierungsdetails wie ausgelöste Ausnahmen zu verbergen, indem sie durch benutzerdefinierte Ausnahmen ersetzt werden). Auch wenn ich zustimme, dass es besser wäre, fangfreien Code zu haben, wie in Pete Beckers Antwort vorgeschlagen , glaube ich nicht, dass Sie die Möglichkeit haben werden, den Code catchund rethrowhier ohne größere Umgestaltungen zu entfernen .

Core-Dump
quelle
1
Die Abgüsse sind nicht nutzlos. Sie sind erforderlich, da kein dynamischer Versand basierend auf dem Argumenttyp erfolgt. Der Argumenttyp muss zum Zeitpunkt der Kompilierung bekannt sein, damit die richtige Methode aufgerufen werden kann.
Joe23
In Ihrer logAndWrapMethode können Sie die Ausnahme auslösen, anstatt sie zurückzugeben, aber den Rückgabetyp beibehalten (Runtime)Exception. Auf diese Weise kann der catch-Block so bleiben, wie Sie ihn geschrieben haben (ohne den nicht erreichbaren Code einzufügen). Aber es hat den zusätzlichen Vorteil, dass Sie das nicht vergessen können throw. Wenn Sie die Ausnahme zurückgeben und den Wurf vergessen, wird die Ausnahme unbemerkt verschluckt. Wenn Sie RuntimeException mit einem Rückgabetyp auslösen, wird der Compiler glücklich, und diese Fehler werden vermieden. So wird Guava Throwables.propagateumgesetzt.
Joe23
@ Joe23 Danke für die Klarstellung zum statischen Versand. Informationen zum Auslösen von Ausnahmen in der Funktion: Bedeuten Sie, dass der mögliche Fehler, den Sie beschreiben, ein catch-Block ist, der aufruft logAndWrap, aber mit der zurückgegebenen Ausnahme nichts tut? Das ist interessant. In einer Funktion, die einen Wert zurückgeben muss, würde der Compiler feststellen, dass es einen Pfad gibt, für den kein Wert zurückgegeben wird. Dies ist jedoch nützlich für Methoden, die nichts zurückgeben. Danke noch einmal.
Coredump
1
Genau das erwähne ich. Und um es noch einmal zu betonen: Es ist nicht etwas, das ich mir ausgedacht habe und das ich gutheiße, sondern das ich aus der Quelle der Guave gewonnen habe.
Joe23
@ Joe23 Ich habe einen expliziten Verweis auf die Bibliothek hinzugefügt
coredump
52

Diese rethrow(e);Funktion verstößt gegen das Prinzip, dass unter normalen Umständen eine Funktion zurückgegeben wird, während unter außergewöhnlichen Umständen eine Funktion eine Ausnahme auslöst. Diese Funktion verletzt dieses Prinzip, indem sie unter normalen Umständen eine Ausnahme auslöst. Das ist die Quelle aller Verwirrung.

Der Compiler geht davon aus, dass diese Funktion unter normalen Umständen zurückgegeben wird, so weit der Compiler erkennen kann, dass die Ausführung möglicherweise das Ende der retrieveUserMailConfigurationFunktion erreicht. An diesem Punkt ist es ein Fehler, keine returnAnweisung zu haben . Das dort RuntimeExceptiongeworfene soll diese Sorge des Compilers lindern, aber es ist ein ziemlich klobiger Weg, es zu tun. Eine andere Möglichkeit, den function must return a valueFehler zu verhindern , besteht darin, eine return null; //to keep the compiler happyErklärung hinzuzufügen , die meiner Meinung nach jedoch ebenso umständlich ist.

Also persönlich würde ich das ersetzen:

rethrow(e);

mit diesem:

report(e); 
throw e;

oder, noch besser, (wie Coredump angedeutet hat) mit diesem:

throw reportAndTransform(e);

Auf diese Weise würde der Steuerungsfluss für den Compiler offensichtlich, sodass Ihr Final throw new RuntimeException("cannot reach here");nicht nur redundant, sondern auch nicht kompilierbar wird, da es vom Compiler als nicht erreichbarer Code gekennzeichnet wird.

Das ist der eleganteste und eigentlich auch einfachste Weg, aus dieser hässlichen Situation herauszukommen.

Mike Nakis
quelle
1
-1 Das Aufteilen der Funktion in zwei Anweisungen kann zu Programmierfehlern führen, die auf fehlerhaftes Kopieren / Einfügen zurückzuführen sind. Ich bin auch ein wenig besorgt darüber, dass Sie Ihre Frage throw reportAndTransform(e)ein paar Minuten nach dem Absenden meiner eigenen Antwort bearbeitet haben. Vielleicht haben Sie Ihre Frage unabhängig davon geändert, und ich entschuldige mich im Voraus, wenn dies der Fall ist, aber ansonsten tun die Leute normalerweise etwas Anerkennung.
Coredump
4
@coredump Tatsächlich habe ich Ihren Vorschlag gelesen, und ich habe sogar eine andere Antwort gelesen, die Ihnen diesen Vorschlag zuschreibt, und ich habe mich daran erinnert, dass ich in der Vergangenheit genau ein solches Konstrukt verwendet habe, und habe beschlossen, es in meine Antwort aufzunehmen. Ich dachte, dass es nicht so wichtig ist, eine Zuschreibung aufzunehmen, weil wir hier nicht über eine originelle Idee sprechen, aber wenn Sie sich damit auseinandersetzen, möchte ich Sie nicht ärgern, also ist die Zuschreibung jetzt da. komplett mit einem link. Prost.
Mike Nakis
Es ist nicht so, dass ich ein Patent für dieses Konstrukt habe, was durchaus üblich ist; aber stell dir vor, du schreibst eine Antwort und nicht lange danach wird sie von einer anderen "assimiliert". Daher wird die Zuschreibung sehr geschätzt. Vielen Dank.
Coredump
3
Es ist nichts falsch per se mit einer Funktion , die nicht zurück. Dies geschieht beispielsweise in Echtzeitsystemen ständig. Das ultimative Problem ist ein Fehler in Java, da die Sprache das Konzept der Korrektheit der Sprachen analysiert und erzwingt, und dennoch bietet die Sprache keinen Mechanismus, um irgendwie zu kommentieren, dass eine Funktion nicht zurückkehrt. Es gibt jedoch Entwurfsmuster, die dies umgehen, wie z throw reportAndTransform(e). Bei vielen Designmustern fehlen Patches und Sprachfunktionen. Das ist einer von ihnen.
David Hammen
2
Andererseits sind viele moderne Sprachen komplex genug. Wenn Sie jedes Designmuster in eine Kernsprache verwandeln, werden diese noch aufgebläht. Dies ist ein gutes Beispiel für ein Entwurfsmuster, das nicht in die Kernsprache gehört. Wie bereits geschrieben, wird es nicht nur vom Compiler, sondern auch von vielen anderen Tools verstanden, die Code analysieren müssen.
MSalters
7

Das throwwurde wahrscheinlich hinzugefügt, um den Fehler "Methode muss einen Wert zurückgeben" zu umgehen, der andernfalls auftreten würde - der Java-Datenflussanalysator ist intelligent genug, um zu verstehen, dass returnnach einem throw, aber nicht nach Ihrer benutzerdefinierten rethrow()Methode, keine @NoReturnAnnotation erforderlich ist dass Sie dies beheben können.

Das Erstellen einer neuen Ausnahme in nicht erreichbarem Code erscheint jedoch überflüssig. Ich würde einfach schreiben return null, wissend, dass es in der Tat nie passiert.

Kilian Foth
quelle
Du hast recht. Ich habe überprüft, was passieren würde, wenn ich die throw new...Zeile auskommentiere und mich über keine Rückgabebescheinigung beschwert. Ist es schlecht programmiert? Gibt es eine Konvention zum Ersetzen einer nicht erreichbaren Rückgabeerklärung? Ich werde diese Frage eine Weile unbeantwortet lassen, um mehr Input zu ermutigen. Trotzdem danke für deine Antwort.
Sok Pomaranczowy
3
@SokPomaranczowy Ja, es ist eine schlechte Programmierung. Diese rethrow()Methode verbirgt die Tatsache, dass catchdie Ausnahme erneut ausgelöst wird. Ich würde so etwas vermeiden. Außerdem kann es natürlich rethrow()passieren, dass die Ausnahme nicht erneut ausgelöst wird. In diesem Fall passiert etwas anderes.
Ross Patterson
26
Bitte ersetzen Sie die Ausnahme nicht durch return null. Mit der Ausnahme, wenn jemand in der Zukunft den Code bricht, damit die letzte Zeile irgendwie erreichbar wird, wird es sofort klar, wenn die Ausnahme ausgelöst wird. Wenn Sie stattdessen return nulljetzt einen nullWert in Ihrer Anwendung haben, der nicht erwartet wurde. Wer weiß, wo in der Bewerbung der NullPointerExceptionWille entsteht? Es sei denn, Sie haben Glück und es ist unmittelbar nach dem Aufruf dieser Funktion, wird es sehr schwierig sein, das eigentliche Problem aufzuspüren.
Unholysampler
5
@MatthewRead Die Ausführung von Code, der nicht erreichbar sein soll, ist ein schwerwiegender Fehler. Es return nullist sehr wahrscheinlich, dass der Fehler ausgeblendet wird, da Sie die anderen Fälle, in denen er nullvon diesem Fehler zurückkehrt, nicht unterscheiden können .
CodesInChaos
4
Wenn die Funktion unter bestimmten Umständen bereits Null zurückgibt und Sie unter diesen Umständen auch eine Nullrückgabe verwenden, gibt es für Benutzer jetzt zwei mögliche Szenarien, in denen sie möglicherweise eine Nullrückgabe erhalten. Manchmal spielt dies keine Rolle, da null return bereits bedeutet, dass es kein Objekt gibt und ich nicht spezifiziere, warum dies so ist. Fügen Sie also einen weiteren möglichen Grund hinzu, warum dies keinen Unterschied macht. Oft ist das Hinzufügen von Mehrdeutigkeiten jedoch schlecht, und es bedeutet sicherlich, dass Fehler zunächst wie "bestimmte Umstände" behandelt werden, anstatt sofort wie "dies ist kaputt" auszusehen. Früh scheitern.
Steve Jessop
6

Das

throw new RuntimeException("cannot reach here");

Die Anweisung macht einem PERSONEN , der den Code liest, klar, was gerade passiert. Es ist also viel besser, als beispielsweise null zurückzugeben. Dies erleichtert auch das Debuggen, wenn der Code auf unerwartete Weise geändert wird.

Scheint aber rethrow(e)einfach falsch! In Ihrem Fall halte ich das Refactoring des Codes für eine bessere Option. In den anderen Antworten (ich mag die besten von coredump) erfahren Sie, wie Sie Ihren Code sortieren können.

Ian
quelle
4

Ich weiß nicht, ob es eine Konvention gibt.

Jedenfalls wäre ein anderer Trick, so zu tun:

private <T> T rethrow(Exception exception) {
    // or whatever it actually does
    Log.e("Ouch! " + exception.getMessage());
    throw new CustomWrapperException(exception);
}

Dies zulassen:

try {
     return translate(mailManagementService.retrieveUserMailConfiguration(id));
} catch (Exception e) {
     return rethrow(e);
}

Und keine künstliche RuntimeExceptionwird mehr benötigt. Obwohl es rethroweigentlich nie einen Wert zurückgibt, ist es jetzt gut genug für den Compiler.

Es gibt theoretisch einen Wert zurück (Methodensignatur) und ist dann davon ausgenommen, da es stattdessen eine Ausnahme auslöst.

Ja, es mag seltsam aussehen, aber RuntimeExceptionandererseits - ein Phantom zu werfen oder Nullen zurückzugeben, die in dieser Welt niemals zu sehen sind - ist auch nicht gerade etwas Schönes.

Wenn Sie die Lesbarkeit anstreben, können Sie Folgendes umbenennen rethrowund haben:

} catch (Exception e) {
     return nothingJustRethrow(e);
}
Konrad Morawski
quelle
2

Wenn Sie den tryBlock komplett entfernen, brauchen Sie das rethrowoder das nicht throw. Dieser Code macht genau das Gleiche wie das Original:

public Configuration retrieveUserMailConfiguration(Long id) throws MailException {
    return translate(mailManagementService.retrieveUserMailConfiguration(id));
}

Lassen Sie sich nicht von der Tatsache täuschen, dass es von erfahreneren Entwicklern stammt. Das ist Code Rot, und das passiert die ganze Zeit. Repariere es einfach.

EDIT: Ich habe falsch gelesen, rethrow(e)als würde ich die Ausnahme einfach neu auslösen e. Wenn diese rethrowMethode tatsächlich etwas anderes ausführt als die Ausnahme erneut auszulösen, ändert das Entfernen der Ausnahme die Semantik dieser Methode.

Pete Becker
quelle
Die Leute werden weiterhin glauben, dass Wildcard-Fänge, die nichts verarbeiten, tatsächlich Ausnahmebehandlungsroutinen sind. Ihre Version ignoriert diese fehlerhafte Annahme, indem sie nicht vorgibt, mit dem umzugehen, was sie nicht kann. Der Aufrufer von retrieveUserMailConfiguration () erwartet, dass er etwas zurückerhält. Wenn diese Funktion nichts Vernünftiges zurückgeben kann, sollte sie schnell und laut ausfallen. Es ist, als ob die anderen Antworten kein Problem mit catch(Exception)einem abscheulichen Code-Geruch erkennen ließen.
msw
1
@msw - Cargo-Kult-Codierung.
Pete Becker
@msw - Auf der anderen Seite können Sie hier einen Haltepunkt setzen.
Pete Becker
1
Es ist, als ob Sie kein Problem damit sehen, einen ganzen Teil der Logik zu verwerfen. Ihre Version macht eindeutig nicht dasselbe wie das Original, was übrigens schon schnell und laut ausfällt. Ich würde es vorziehen, catch-free-Methoden wie die in Ihrer Antwort zu schreiben, aber wenn wir mit vorhandenem Code arbeiten, sollten wir Verträge mit anderen Arbeitsteilen nicht brechen. Es kann sein, dass das, was in getan rethrowwird, nutzlos ist (und das ist hier nicht der Punkt), aber Sie können nicht einfach Code loswerden, den Sie nicht mögen, und sagen, es ist gleichbedeutend mit dem Original, wenn es eindeutig nicht ist. Es gibt Nebenwirkungen in der benutzerdefinierten rethrowFunktion.
Coredump
1
@ jpmc26 Der Kern der Frage ist die Ausnahme "kann hier nicht erreichen", die neben dem vorhergehenden try / catch-Block auch aus anderen Gründen auftreten kann. Aber ich stimme zu, dass der Block fischig riecht, und wenn diese Antwort ursprünglich sorgfältiger formuliert worden wäre, hätte sie viel mehr positive Stimmen erhalten (ich habe übrigens nicht negative Stimmen abgegeben). Die letzte Bearbeitung ist gut.
Coredump