¿Lanzar nuevas RuntimeExceptions en código inalcanzable es un mal estilo?

31

Me asignaron a mantener una aplicación escrita hace algún tiempo por desarrolladores más calificados. Me encontré con este código:

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

Tengo curiosidad si el lanzamiento RuntimeException("cannot reach here")está justificado. Probablemente me estoy perdiendo algo obvio sabiendo que este código proviene de un colega más experimentado.
EDITAR: Aquí hay que volver a lanzar el cuerpo al que se refieren algunas respuestas. Considero que no es importante en esta pregunta.

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
fuente
12
es técnicamente accesible, si rethrowno logra throwuna excepción. (lo que puede suceder algún día, si la implementación cambia)
njzk2
34
Por otro lado, un AssertionError podría ser una opción semánticamente mejor que RuntimeException.
JvR
1
El tiro final es redundante. Si habilita findbugs u otras herramientas de análisis estático, marcaría estos tipos de líneas para su eliminación.
Bon Ami
77
Ahora que veo el resto del código, estoy pensando que tal vez debería cambiar "los desarrolladores más calificados" en "más altos a los desarrolladores". La revisión de código con gusto explicará por qué.
JvR
3
He intentado crear ejemplos hipotéticos de por qué las excepciones son terribles. Pero nada de lo que he hecho tiene nada que ver con esto.
Paul Draper

Respuestas:

36

Primero, gracias por responder su pregunta y mostrarnos qué rethrowhace. Entonces, de hecho, lo que hace es convertir excepciones con propiedades en clases de excepciones más finas. Más sobre esto más tarde.

Como no respondí realmente a la pregunta principal originalmente, aquí va: sí, generalmente es un mal estilo lanzar excepciones de tiempo de ejecución en código inalcanzable; será mejor que use afirmaciones, o incluso mejor, evite el problema. Como ya se señaló, el compilador aquí no puede estar seguro de que el código nunca salga del try/catchbloque. Puede refactorizar su código aprovechando que ...

Los errores son valores

(Como era de esperar, es bien conocido en ir )

Usemos un ejemplo más simple, el que usé antes de su edición: así que imagine que está registrando algo y creando una excepción de contenedor como en la respuesta de Konrad . Digamos que es logAndWrap.

En lugar de lanzar la excepción como un efecto secundario de logAndWrap, podría dejar que haga su trabajo como un efecto secundario y hacer que devuelva una excepción (al menos, la que aparece en la entrada). No necesita usar genéricos, solo funciones básicas:

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

Entonces, throwexplícitamente, y su compilador está contento:

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

¿Qué pasa si te olvidas throw?

Como se explica en el comentario de Joe23 , una forma de programación defensiva para garantizar que siempre se lance la excepción consistiría en hacer explícitamente un throw new CustomWrapperException(exception)al final de logAndWrap, como lo hace Guava . De esa manera, sabrá que se lanzará la excepción y su analizador de tipos estará contento. Sin embargo, sus excepciones personalizadas deben ser excepciones no marcadas, lo que no siempre es posible. Además, calificaría el riesgo de que faltara un desarrollador para escribir throwcomo muy bajo: el desarrollador debe olvidarlo y el método circundante no debería devolver nada; de lo contrario, el compilador detectaría un retorno faltante. Sin embargo, esta es una forma interesante de luchar contra el sistema de tipos, y funciona.

Rethrow

El actual también rethrowse puede escribir como una función, pero tengo problemas con su implementación actual:

  • Hay muchos moldes inútiles como De hecho, se requieren moldes (ver comentarios):

    if (e instanceof ServiceUnavailableException) {
        ServiceUnavailableException ex = (ServiceUnavailableException) e;
        rethrow(ex);
    }
  • Al lanzar / devolver nuevas excepciones, la anterior se descarta; en el siguiente código, a MailLoginNotAvailableExceptionno me permite saber qué inicio de sesión no está disponible, lo cual es inconveniente; además, los seguimientos de pila estarán incompletos:

    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();
        }
    }
  • ¿Por qué el código de origen no arroja esas excepciones especializadas en primer lugar? Sospecho que rethrowse usa como una capa de compatibilidad entre un subsistema (de correo) y la lógica de negocios (tal vez la intención es ocultar detalles de implementación como excepciones lanzadas reemplazándolas por excepciones personalizadas). Incluso si estoy de acuerdo en que sería mejor tener un código libre de capturas, como se sugiere en la respuesta de Pete Becker , no creo que tenga la oportunidad de eliminar el código catchy rethrowaquí sin mayores refactorizaciones.

volcado de memoria
fuente
1
Los moldes no son inútiles. Son obligatorios porque no existe un despacho dinámico basado en el tipo de argumento. El tipo de argumento debe conocerse en tiempo de compilación para llamar al método correcto.
Joe23
En su logAndWrapmétodo, podría lanzar la excepción en lugar de devolverla, pero mantenga el tipo de devolución (Runtime)Exception. De esta forma, el bloque catch puede permanecer de la forma en que lo escribió (sin el lanzamiento de un código inalcanzable). Pero tiene la ventaja adicional de que no se puede olvidar throw. Si devuelve la excepción y olvida el lanzamiento, esto provocará que la excepción se trague en silencio. Lanzar mientras se tiene un tipo de retorno RuntimeException hará feliz al compilador al tiempo que evita estos errores. Así Throwables.propagatese implementa la guayaba .
Joe23
@ Joe23 Gracias por la aclaración sobre el envío estático. Acerca de lanzar excepciones dentro de la función: ¿quiere decir que el posible error que está describiendo es un bloque catch que llamalogAndWrap pero no hace nada con la excepción devuelta? Eso es interesante. Dentro de una función que debe devolver un valor, el compilador notaría que hay una ruta sin que se devuelva ningún valor, pero esto es útil para métodos que no devuelven nada. Gracias de nuevo.
coredump
1
Eso es exactamente lo que digo. Y solo para enfatizar el punto nuevamente: no es algo de lo que se me ocurrió y de lo que me doy crédito, sino de la fuente de la guayaba.
Joe23
@ Joe23 agregué una referencia explícita a la biblioteca
coredump
52

Esta rethrow(e);función viola el principio que dice que en circunstancias normales, una función regresará, mientras que en circunstancias excepcionales, una función arrojará una excepción. Esta función viola este principio al lanzar una excepción en circunstancias normales. Esa es la fuente de toda la confusión.

El compilador asume que esta función volverá en circunstancias normales, por lo que hasta donde el compilador puede decir, la ejecución puede llegar al final de la retrieveUserMailConfigurationfunción, en cuyo punto es un error no tener una returndeclaración. Se RuntimeExceptionsupone que arrojarlo alivia esta preocupación del compilador, pero es una forma bastante torpe de hacerlo. Otra forma de prevenir el function must return a valueerror es agregar una return null; //to keep the compiler happydeclaración, pero eso es igualmente torpe en mi opinión.

Entonces, personalmente, reemplazaría esto:

rethrow(e);

con este:

report(e); 
throw e;

o, mejor aún, (como sugirió coredump ,) con esto:

throw reportAndTransform(e);

Por lo tanto, el flujo de control se haría obvio para el compilador, por lo que su final throw new RuntimeException("cannot reach here");se volvería no solo redundante, sino que en realidad ni siquiera sería compilable, ya que el compilador lo marcaría como código inalcanzable.

Esa es la forma más elegante y en realidad también más simple de salir de esta fea situación.

Mike Nakis
fuente
1
-1 Sugerir dividir la función en dos declaraciones puede introducir errores de programación debido a una mala copia / pegado; También estoy un poco preocupado por el hecho de que haya editado su pregunta para sugerir throw reportAndTransform(e)un par de minutos después de que publiqué mi propia respuesta. Tal vez modificó su pregunta de forma independiente, y me disculpo de antemano si es así, pero de lo contrario, dar un poco de crédito es algo que la gente suele hacer.
coredump
44
@coredump De hecho, leí tu sugerencia, e incluso leí otra respuesta que te atribuye esta sugerencia, y recordé que en realidad he empleado precisamente tal construcción en el pasado, así que decidí incluirla en mi respuesta. Pensé que no es tan importante como para incluir una atribución, porque no estamos hablando de una idea original aquí, pero si tienes problemas con eso, no quiero agravarte, por lo que la atribución ya está allí, Completo con un enlace. Aclamaciones.
Mike Nakis
No es que tenga una patente para esta construcción, que es bastante común; pero imagina que escribes una respuesta y no mucho después, es "asimilada" por otra. Entonces la atribución es muy apreciada. Gracias.
coredump
3
No hay nada malo per se con una función que no regrese. Sucede todo el tiempo, por ejemplo, en sistemas de tiempo real. El último problema es una falla en Java en que el lenguaje analiza y aplica el concepto de corrección de los lenguajes y, sin embargo, el lenguaje no proporciona un mecanismo para anotar de alguna manera que una función no regresa. Sin embargo, hay patrones de diseño que evitan esto, como throw reportAndTransform(e). Muchos patrones de diseño son parches, faltan características del lenguaje. Este es uno de ellos.
David Hammen
2
Por otro lado, muchos idiomas modernos son lo suficientemente complejos. Convertir cada patrón de diseño en una característica central del lenguaje los hinchará aún más. Este es un buen ejemplo de un patrón de diseño que no pertenece al lenguaje central. Tal como está escrito, se entiende bien no solo por el compilador, sino también por muchas otras herramientas que tienen que analizar el código.
MSalters
7

El throwprobablemente se añadió a moverse por el "método debe devolver un valor" error que ocurriría de otra manera - los datos de Java fluyen analizador es lo suficientemente inteligente como para entender que no returnes necesario después de una throw, pero no después de su costumbre rethrow()método, y no hay ninguna @NoReturnanotación que podrías usar para arreglar esto.

Sin embargo, crear una nueva Excepción en código inalcanzable parece superfluo. Simplemente escribiría return null, sabiendo que, de hecho, nunca sucede.

Kilian Foth
fuente
Tienes razón. Verifiqué lo que sucedería si comentara la throw new...línea y se queja de que no hay declaración de devolución. ¿Es mala programación? ¿Existe alguna convención sobre la sustitución de una declaración de retorno inalcanzable? Dejaré esta pregunta sin respuesta por un tiempo para alentar más comentarios. Sin embargo, gracias por tu respuesta.
Sok Pomaranczowy
3
@SokPomaranczowy Sí, es una mala programación. Ese rethrow()método oculta el hecho de que catchestá volviendo a lanzar la excepción. Evitaría hacer algo así. Además, por supuesto, en rethrow()realidad podría no volver a lanzar la excepción, en cuyo caso sucede algo más.
Ross Patterson
26
Por favor, no reemplace la excepción con return null. Con la excepción, si alguien en el futuro rompe el código para que la última línea sea accesible, quedará claro de inmediato cuando se lanza la excepción. Si en su lugar return null, ahora tiene un nullvalor flotando en su aplicación que no se esperaba. ¿Quién sabe dónde NullPointerExceptionsurgirá en la aplicación ? A menos que tenga suerte y sea media después de la llamada a esta función, será muy difícil localizar el problema real.
unholysampler
55
@MatthewRead La ejecución del código destinado a ser inalcanzable es un error grave, return nulles muy probable que enmascare el error porque no puede distinguir los otros casos en los que regresa nullde este error.
CodesInChaos
44
Además, si la función ya devuelve nulo en algunas circunstancias, y también utiliza un retorno nulo en esta circunstancia, los usuarios ahora tienen dos posibles escenarios en los que podrían encontrarse cuando obtienen un retorno nulo. A veces esto no importa, porque el retorno nulo ya significa "no hay ningún objeto y no estoy especificando por qué", por lo que agregar una posible razón más por qué hace poca diferencia. Pero a menudo agregar ambigüedad es malo, y ciertamente significa que los errores inicialmente serán tratados como "ciertas circunstancias" en lugar de parecer inmediatamente "esto está roto". Fallar temprano
Steve Jessop
6

los

throw new RuntimeException("cannot reach here");

declaración deja en claro a una PERSONA lee el código lo que está sucediendo, por lo que es mucho mejor que devolver nulo, por ejemplo. También facilita la depuración si el código se cambia de forma inesperada.

Sin embargo, ¡ rethrow(e)parece estar mal! Entonces, en su caso , creo que refactorizar el código es una mejor opción. Consulte las otras respuestas (me gusta lo mejor de coredump) para obtener formas de ordenar su código.

Ian
fuente
4

No sé si hay una convención.

De todos modos, otro truco sería hacerlo así:

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

Teniendo en cuenta esto:

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

Y ya no RuntimeExceptionse necesita artificial . Aunque en rethrowrealidad nunca devuelve ningún valor, ahora es lo suficientemente bueno para el compilador.

Devuelve un valor en teoría (firma del método), y luego está exento de hacerlo, ya que arroja una excepción.

Sí, puede parecer extraño, pero de nuevo, arrojar un fantasma RuntimeExceptiono devolver nulos que nunca se verán en este mundo, tampoco es exactamente una belleza.

En busca de la legibilidad, puede cambiar el nombre rethrowy tener algo como:

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

Si elimina el trybloque por completo, entonces no necesita el rethrowo el throw. Este código hace exactamente lo mismo que el original:

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

No te dejes engañar por el hecho de que proviene de desarrolladores más experimentados. Esta es la descomposición del código, y ocurre todo el tiempo. Solo arréglalo.

EDITAR: leí mal rethrow(e)como simplemente volver a lanzar la excepción e. Si ese rethrowmétodo realmente hace algo más que volver a lanzar la excepción, deshacerse de él cambia la semántica de este método.

Pete Becker
fuente
La gente seguirá creyendo que las capturas de comodines que no manejan nada son en realidad manejadores de excepciones. Su versión ignora esa suposición defectuosa al no pretender manejar lo que no puede. La persona que llama de retrieveUserMailConfiguration () espera recuperar algo. Si esa función no puede devolver algo razonable, debería fallar rápida y ruidosamente. Es como si las otras respuestas no vieran ningún problema con lo catch(Exception)que es un olor a código odioso.
msw
1
@msw - Codificación de culto de carga.
Pete Becker
@msw: por otro lado, te da un lugar para establecer un punto de interrupción.
Pete Becker
1
Es como si no vieras ningún problema descartando una parte completa de la lógica. Claramente, su versión no hace lo mismo que el original, que por cierto, ya falla rápidamente. Prefiero escribir métodos libres de capturas como el que aparece en su respuesta, pero cuando trabajemos con código existente, no deberíamos romper los contratos con otras partes en funcionamiento. Puede ser que lo que se hace rethrowsea ​​inútil (y este no es el punto aquí), pero no puede deshacerse del código que no le gusta y decir que es equivalente al original cuando claramente no lo es. Hay efectos secundarios en la rethrowfunción personalizada .
coredump
1
@ jpmc26 El núcleo de la pregunta es sobre la excepción "no se puede llegar aquí", que podría surgir por otras razones además del bloque try / catch anterior. Pero estoy de acuerdo en que el bloque huele a pescado, y si esta respuesta fue redactada más cuidadosamente originalmente, habría ganado muchos más votos a favor (no voté a favor, por cierto). La última edición es buena.
coredump