it-swarm.com.de

Ist es ein schlechter Stil, neue RuntimeExceptions in nicht erreichbaren Code zu werfen?

Ich wurde beauftragt, eine Anwendung zu pflegen, 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 von RuntimeException("cannot reach here") gerechtfertigt ist. Ich vermisse wahrscheinlich etwas Offensichtliches, weil 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 in dieser Frage für nicht wichtig.

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();
    }
}
31

Zunächst einmal vielen Dank, dass Sie Ihre Frage beantwortet und uns gezeigt haben, was rethrow bewirkt. 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, geht es hier so: Ja, es ist im Allgemeinen ein schlechter Stil, Laufzeitausnahmen in nicht erreichbaren Code zu werfen; 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 Block try/catch Verlässt. Sie können Ihren Code umgestalten, indem Sie den Vorteil nutzen, dass ...

Fehler sind Werte

(Es überrascht nicht, es ist in go bekannt )

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

Anstatt die Ausnahme als Nebeneffekt von logAndWrap auszulösen, können Sie sie als Nebeneffekt arbeiten 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 throw explizit, und Ihr Compiler ist glücklich:

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

Was ist, wenn Sie throw vergessen?

Wie in Joe23s Kommentar erläutert, besteht eine defensive Programmierung Möglichkeit, um sicherzustellen, dass die Ausnahme immer ausgelöst wird, darin, explizit eine throw new CustomWrapperException(exception) at auszuführen das Ende von logAndWrap, wie es von Guava.Throwables gemacht wird. Auf diese Weise wissen Sie, dass die Ausnahme ausgelöst wird, und Ihr Typanalysator ist zufrieden. Ihre benutzerdefinierten Ausnahmen müssen jedoch deaktivierte Ausnahmen sein, was nicht immer möglich ist. Außerdem würde ich das Risiko, dass ein Entwickler fehlt, um throw zu schreiben, als sehr gering bewerten: Der Entwickler muss es vergessen und die umgebende Methode sollte nicht zurückkehren alles, sonst würde der Compiler eine fehlende Rückgabe erkennen. Dies ist ein interessanter Weg, um das Typensystem zu bekämpfen, und es funktioniert jedoch.

Neu werfen

Der tatsächliche rethrow kann auch als Funktion geschrieben werden, aber ich habe Probleme mit der aktuellen Implementierung:

  • Es gibt viele nutzlose Casts wie Abgüsse sind tatsächlich erforderlich (siehe Kommentare):

    if (e instanceof ServiceUnavailableException) {
        ServiceUnavailableException ex = (ServiceUnavailableException) e;
        rethrow(ex);
    }
    
  • Beim Auslösen/Zurückgeben neuer Ausnahmen wird die alte verworfen. Im folgenden Code kann ich mit einem MailLoginNotAvailableException nicht wissen, 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 rethrow als Kompatibilitätsschicht zwischen einem (Mailing-) Subsystem und einer Geschäftslogik verwendet wird (möglicherweise besteht die Absicht darin, Implementierungsdetails wie ausgelöste Ausnahmen zu verbergen, indem sie durch benutzerdefinierte Ausnahmen ersetzt werden). Selbst wenn ich zustimme, dass es wäre besser, einen abfangfreien Code zu haben, wie in Pete Beckers Antwort vorgeschlagen , glaube ich nicht, dass Sie die Möglichkeit haben, den Code catch und rethrow zu entfernen hier ohne größere Umgestaltungen.

36
coredump

Diese Funktion rethrow(e); verstößt gegen das Prinzip, dass unter normalen Umständen eine Funktion zurückgegeben wird, während eine Funktion unter außergewöhnlichen Umständen eine Ausnahme auslöst. Diese Funktion verstößt gegen 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. Soweit der Compiler dies beurteilen kann, kann die Ausführung das Ende der Funktion retrieveUserMailConfiguration erreichen. An diesem Punkt ist es ein Fehler, kein return Anweisung. Das dort geworfene RuntimeException soll diese Besorgnis des Compilers lindern, aber es ist eine ziemlich klobige Art, dies zu tun. Eine andere Möglichkeit, den Fehler function must return a value Zu verhindern, besteht darin, eine return null; //to keep the compiler happy - Anweisung hinzuzufügen, die meiner Meinung nach jedoch ebenso umständlich ist.

Ich persönlich würde dies also ersetzen:

rethrow(e);

mit diesem:

report(e); 
throw e;

oder, noch besser, (wie Coredump vorgeschlagen ,) mit diesem:

throw reportAndTransform(e);

Auf diese Weise würde der Kontrollfluss dem Compiler klar gemacht, sodass Ihre endgültige throw new RuntimeException("cannot reach here"); nicht nur redundant, sondern auch nicht kompilierbar wäre, da sie vom Compiler als nicht erreichbarer Code gekennzeichnet würde.

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

52
Mike Nakis

Das throw wurde wahrscheinlich hinzugefügt, um den Fehler "Methode muss einen Wert zurückgeben" zu umgehen, der sonst auftreten würde - der Java Datenflussanalysator ist intelligent genug, um zu verstehen, dass kein return ist nach einem throw erforderlich, jedoch nicht nach Ihrer benutzerdefinierten rethrow() -Methode, und es gibt kein @NoReturn Anmerkung, mit der 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 tatsächlich nie passiert.

7
Kilian Foth

Das

throw new RuntimeException("cannot reach here");

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

Allerdings scheint rethrow(e) einfach falsch zu sein! Also in Ihrem Fall Ich denke, das Refactoring des Codes ist eine bessere Option. In den anderen Antworten (ich mag Coredumps am besten) finden Sie Möglichkeiten, Ihren Code zu sortieren.

6
Ian

Ich weiß nicht, ob es eine Konvention gibt.

Ein weiterer Trick wäre jedenfalls, dies zu tun:

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

Erlaubt dies:

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

Und es wird kein künstliches RuntimeException mehr benötigt. Obwohl rethrow eigentlich 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 stattdessen eine Ausnahme ausgelöst wird.

Ja, es mag seltsam aussehen, aber andererseits - ein Phantom RuntimeException zu werfen oder Nullen zurückzugeben, die auf dieser Welt niemals zu sehen sind - ist auch nicht gerade eine Sache der Schönheit.

Im Streben nach Lesbarkeit könnten Sie rethrow umbenennen und so etwas wie:

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

Wenn Sie den Block try vollständig entfernen, benötigen Sie weder rethrow noch 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. Dies ist Code Rot, und es passiert die ganze Zeit. Repariere es einfach.

EDIT: Ich habe rethrow(e) falsch gelesen, indem ich einfach die Ausnahme e erneut ausgelöst habe. Wenn diese rethrow -Methode tatsächlich etwas anderes tut als die Ausnahme erneut auszulösen, ändert das Entfernen der Ausnahme die Semantik dieser Methode.

2
Pete Becker