Solución para las excepciones comprobadas de Java

49

Aprecio mucho las nuevas características de Java 8 sobre lambdas e interfaces de métodos predeterminados. Sin embargo, todavía me aburro con las excepciones marcadas. Por ejemplo, si solo quiero enumerar todos los campos visibles de un objeto, me gustaría simplemente escribir esto:

    Arrays.asList(p.getClass().getFields()).forEach(
        f -> System.out.println(f.get(p))
    );

Sin embargo, dado que el getmétodo podría arrojar una excepción marcada, que no está de acuerdo con el Consumercontrato de interfaz, entonces debo detectar esa excepción y escribir el siguiente código:

    Arrays.asList(p.getClass().getFields()).forEach(
            f -> {
                try {
                    System.out.println(f.get(p));
                } catch (IllegalArgumentException | IllegalAccessException ex) {
                    throw new RuntimeException(ex);
                }
            }
    );

Sin embargo, en la mayoría de los casos, solo quiero que la excepción se arroje como ay RuntimeExceptiondeje que el programa maneje, o no, la excepción sin errores de compilación.

Por lo tanto, me gustaría tener su opinión sobre mi solución controvertida para las molestias de excepciones comprobadas. Con ese fin, creé una interfaz auxiliar ConsumerCheckException<T>y una función de utilidad rethrow( actualizada de acuerdo con la sugerencia del comentario de Doval ) de la siguiente manera:

  @FunctionalInterface
  public interface ConsumerCheckException<T>{
      void accept(T elem) throws Exception;
  }

  public class Wrappers {
      public static <T> Consumer<T> rethrow(ConsumerCheckException<T> c) {
        return elem -> {
          try {
            c.accept(elem);
          } catch (Exception ex) {
            /**
             * within sneakyThrow() we cast to the parameterized type T. 
             * In this case that type is RuntimeException. 
             * At runtime, however, the generic types have been erased, so 
             * that there is no T type anymore to cast to, so the cast
             * disappears.
             */
            Wrappers.<RuntimeException>sneakyThrow(ex);
          }
        };
      }

      /**
       * Reinier Zwitserloot who, as far as I know, had the first mention of this
       * technique in 2009 on the java posse mailing list.
       * http://www.mail-archive.com/[email protected]/msg05984.html
       */
      public static <T extends Throwable> T sneakyThrow(Throwable t) {
          throw (T) t;
      }
  }

Y ahora solo puedo escribir:

    Arrays.asList(p.getClass().getFields()).forEach(
            rethrow(f -> System.out.println(f.get(p)))
    );

No estoy seguro de que este sea el mejor modismo para cambiar las excepciones marcadas, pero como expliqué, me gustaría tener una forma más conveniente de lograr mi primer ejemplo sin tratar con las excepciones marcadas y esta es la forma más simple que encontré para hacerlo.

Fernando Miguel Carvalho
fuente
2
Además del enlace de Robert, también eche un vistazo a Excepciones marcadas disimuladamente . Si quisieras, puedes usar sneakyThrowdentro rethrowpara lanzar la excepción original, marcada, en lugar de envolverla en un RuntimeException. Alternativamente, podría usar la @SneakyThrowsanotación del Proyecto Lombok que hace lo mismo.
Doval
1
También tenga en cuenta que la Consumers en forEachpuede ejecutarse en forma paralela cuando se usa Streams paralela . Un lanzamiento arrojado desde el consumidor se propagará al hilo de llamada, que 1) no detendrá a los otros consumidores que se ejecutan simultáneamente, lo que puede ser apropiado o no, y 2) si más de uno de los consumidores arroja algo, solo uno de los lanzamientos será visto por el hilo de llamada.
Joonas Pulakka
2
Las lambdas son tan feas.
Tulains Córdova

Respuestas:

16

Ventajas, desventajas y limitaciones de su técnica:

  • Si el código de llamada es para manejar la excepción marcada, DEBE agregarlo a la cláusula throws del método que contiene la secuencia. El compilador ya no te obligará a agregarlo, por lo que es más fácil olvidarlo. Por ejemplo:

    public void test(Object p) throws IllegalAccessException {
        Arrays.asList(p.getClass().getFields()).forEach(rethrow(f -> System.out.println(f.get(p))));
    }
  • Si el código de llamada ya maneja la excepción marcada, el compilador le recordará que agregue la cláusula throws a la declaración del método que contiene la secuencia (si no lo hace, dirá: la excepción nunca se arroja en el cuerpo de la instrucción try correspondiente) .

  • En cualquier caso, no podrá rodear la secuencia en sí para detectar la excepción marcada DENTRO del método que contiene la secuencia (si lo intenta, el compilador dirá: La excepción nunca se arroja en el cuerpo de la declaración de prueba correspondiente).

  • Si está llamando a un método que literalmente nunca puede lanzar la excepción que declara, entonces no debe incluir la cláusula throws. Por ejemplo: new String (byteArr, "UTF-8") produce UnsupportedEncodingException, pero la especificación de Java garantiza que UTF-8 siempre esté presente. Aquí, la declaración de tiros es una molestia y cualquier solución para silenciarla con un mínimo repetitivo es bienvenida.

  • Si odia las excepciones marcadas y cree que nunca deberían agregarse al lenguaje Java para empezar (un número creciente de personas piensan de esta manera, y NO soy uno de ellos), entonces no agregue la excepción marcada a los lanzamientos cláusula del método que contiene la secuencia. La excepción marcada se comportará como una excepción NO marcada.

  • Si está implementando una interfaz estricta donde no tiene la opción de agregar una declaración de lanzamiento y, sin embargo, lanzar una excepción es completamente apropiado, entonces envolver una excepción solo para obtener el privilegio de lanzarlo da como resultado un seguimiento de pila con excepciones espurias que no aportan información sobre lo que realmente salió mal. Un buen ejemplo es Runnable.run (), que no arroja ninguna excepción marcada. En este caso, puede decidir no agregar la excepción marcada a la cláusula throws del método que contiene la secuencia.

  • En cualquier caso, si decide NO agregar (u olvidarse de agregar) la excepción marcada a la cláusula throws del método que contiene la secuencia, tenga en cuenta estas 2 consecuencias de lanzar excepciones CHECKED:

    1. El código de llamada no podrá capturarlo por su nombre (si lo intenta, el compilador dirá: la excepción nunca se arroja en el cuerpo de la instrucción de prueba correspondiente). Burbujeará y probablemente quedará atrapado en el bucle principal del programa por alguna "excepción de captura" o "captura Throwable", que puede ser lo que desee de todos modos.

    2. Viola el principio de la menor sorpresa: ya no será suficiente capturar RuntimeException para garantizar la captura de todas las posibles excepciones. Por esta razón, creo que esto no debe hacerse en el código marco, sino solo en el código comercial que usted controla por completo.

Referencias

NOTA: Si decide utilizar esta técnica, puede copiar la LambdaExceptionUtilclase auxiliar de StackOverflow: https://stackoverflow.com/questions/27644361/how-can-i-throw-checked-exceptions-from-inside-java-8 -corrientes . Le brinda la implementación completa (Función, Consumidor, Proveedor ...), con ejemplos.

MarcG
fuente
6

En este ejemplo, ¿puede realmente fallar alguna vez? No lo creas, pero quizás tu caso sea especial. Si lo que realmente "no puede fallar", y es sólo una cosa compilador molesto, me gusta envolver la excepción y lanzar una Errorcon un comentario "no puede suceder". Deja las cosas muy claras para el mantenimiento. De lo contrario, se preguntarán "¿cómo puede suceder esto" y "quién diablos maneja esto?"

Esto en una práctica controvertida por lo que YMMV. Probablemente obtendré algunos votos negativos.

user949300
fuente
3
+1 porque arrojarás un error. ¿Cuántas veces he terminado después de horas de depuración en un bloque de captura que contiene un solo comentario de línea "no puede suceder" ...
Axel
3
-1 porque arrojarás un error. Los errores son para indicar que algo está mal en JVM y los niveles superiores pueden manejarlos en consecuencia. Si elige de esa manera, debe lanzar RuntimeException. Otra posible solución son las afirmaciones (necesidad-marca de alerta habilitada) o el registro.
duros
3
@duros: Dependiendo de por qué la excepción "no puede suceder", el hecho de que se lance puede indicar que algo está muy mal. Supongamos, por ejemplo, si uno llama clonea un tipo sellado Fooque se sabe que lo admite, y arroja CloneNotSupportedException. ¿Cómo podría suceder eso a menos que el código se vincule con algún otro tipo inesperado Foo? Y si eso sucede, ¿se puede confiar en algo?
supercat
1
@supercat excelente ejemplo. Otros están lanzando excepciones por faltar codificaciones de cadena o mensajes de mensaje. Si faltan UTF-8 o SHA, es probable que el tiempo de ejecución esté dañado.
user949300
1
@duros Esa es una idea falsa completa. An Error is a subclass of Throwable that indicates serious problems that a reasonable application should not try to catch.(Citado del Javadoc de Error) Esta es exactamente esa clase de situación, por lo tanto, lejos de ser inadecuada, lanzar un Erroraquí es la opción más adecuada.
biziclop
1

otra versión de este código donde la excepción marcada solo se retrasa:

public class Cocoon {
         static <T extends Throwable> T forgetThrowsClause(Throwable t) throws T{
            throw (T) t;
        }

        public static <X, T extends Throwable> Consumer<X> consumer(PeskyConsumer<X,T> touchyConsumer) throws T {
            return new Consumer<X>() {
                @Override
                public void accept(X t) {
                    try {
                        touchyConsumer.accept(t);
                    } catch (Throwable exc) {
                        Cocoon.<RuntimeException>forgetThrowsClause(exc) ;
                    }

                }
            } ;
        }
// and so on for Function, and other codes from java.util.function
}

la magia es que si llamas:

 myArrayList.forEach(Cocoon.consumer(MyClass::methodThatThrowsException)) ;

entonces su código estará obligado a detectar la excepción

oso java
fuente
¿Qué sucede si esto se ejecuta en una secuencia paralela donde los elementos se consumen en diferentes subprocesos?
prunge
0

sneakyThrow es genial! Entiendo totalmente tu dolor.

Si tienes que quedarte en Java ...

Paguro tiene interfaces funcionales que envuelven excepciones comprobadas para que no tenga que pensar más en esto. Incluye colecciones inmutables y transformaciones funcionales a lo largo de las líneas de transductores Clojure (o Java 8 Streams) que toman las interfaces funcionales y envuelven excepciones comprobadas.

También hay VAVr y The Eclipse Collections para probar.

De lo contrario, use Kotlin

Kotlin es compatible de 2 vías con Java, no tiene excepciones comprobadas, no tiene primitivas (bueno, no es que el programador tenga que pensar), un mejor sistema de tipos, asume inmutabilidad, etc. A pesar de que selecciono la biblioteca de Paguro anterior, yo ' Estoy convirtiendo todo el código Java que puedo a Kotlin. Todo lo que solía hacer en Java ahora prefiero hacerlo en Kotlin.

GlenPeterson
fuente
Alguien rechazó este voto, lo cual está bien, pero no aprenderé nada a menos que me diga por qué lo rechazó.
GlenPeterson
1
+1 para su biblioteca en github, puede consultar también eclipse.org/collections o project vavr que proporcionan colecciones funcionales e inmutables. Cuales son tus pensamientos sobre estos ?
firephil
-1

Las excepciones marcadas son muy útiles y su manejo depende del tipo de producto del que forma parte:

  • una biblioteca
  • una aplicación de escritorio
  • un servidor que se ejecuta dentro de una caja
  • un ejercicio academico

Por lo general, una biblioteca no debe manejar excepciones comprobadas, sino declarar como arrojada por API pública. El raro caso en que podrían envolverse en RuntimeExcepton simple es, por ejemplo, crear dentro del código de la biblioteca algún documento xml privado y analizarlo, crear un archivo temporal y luego leerlo.

Para las aplicaciones de escritorio, debe comenzar el desarrollo del producto creando su propio marco de manejo de excepciones. Usando su propio marco, generalmente envolverá una excepción marcada en algunos de dos a cuatro envoltorios (sus subclases personalizadas de RuntimeExcepion) y las manejará con un solo controlador de excepciones.

Para los servidores, generalmente su código se ejecutará dentro de algún marco. Si no utiliza ninguno (un servidor básico), cree su propio marco similar (basado en tiempos de ejecución) descrito anteriormente.

Para ejercicios académicos, siempre puede envolverlos directamente en RuntimeExcepton. Alguien puede crear un pequeño marco también.

Razmik
fuente
-1

Es posible que desee echar un vistazo a este proyecto ; Lo creé específicamente debido al problema de las excepciones comprobadas y las interfaces funcionales.

Con él puedes hacer esto en lugar de escribir tu código personalizado:

// I don't know the type of f, so it's Foo...
final ThrowingConsumer<Foo> consumer = f -> System.out.println(f.get(p));

Dado que todas esas interfaces Throwing * extienden sus contrapartes no Throwing, puede usarlas directamente en la secuencia:

Arrays.asList(p.getClass().getFields()).forEach(consumer);

O simplemente puedes:

import static com.github.fge.lambdas.consumers.Consumers.wrap;

Arrays.asList(p.getClass().getFields())
    .forEach(wrap(f -> System.out.println(f.get(p)));

Y otras cosas.

fge
fuente
Mejor aúnfields.forEach((ThrowingConsumer<Field>) f -> out.println(f.get(o)))
usuario2418306
¿Podrían los downvoters explicar por favor?
fge