Java 8: control obligatorio de excepciones comprobadas en expresiones lambda. ¿Por qué obligatorio, no opcional?

Estoy jugando con las nuevas características de lambda en Java 8, y descubrí que las prácticas que ofrece Java 8 son realmente útiles. Sin embargo, me pregunto si existe una buena manera de solucionar el siguiente escenario. Supongamos que tiene un contenedor de grupos de objetos que requiere algún tipo de fábrica para llenar el grupo de objetos, por ejemplo (usando java.lang.functions.Factory ):

 public class JdbcConnectionPool extends ObjectPool { public ConnectionPool(int maxConnections, String url) { super(new Factory() { @Override public Connection make() { try { return DriverManager.getConnection(url); } catch ( SQLException ex ) { throw new RuntimeException(ex); } } }, maxConnections); } } 

Después de transformar la interfaz funcional en expresión lambda, el código anterior es así:

 public class JdbcConnectionPool extends ObjectPool { public ConnectionPool(int maxConnections, String url) { super(() -> { try { return DriverManager.getConnection(url); } catch ( SQLException ex ) { throw new RuntimeException(ex); } }, maxConnections); } } 

No es tan malo, pero la excepción comprobada java.sql.SQLException requiere un bloque try / catch dentro de la lambda. En mi empresa usamos dos interfaces por mucho tiempo:

  • IOut que es equivalente a java.lang.functions.Factory ;
  • y una interfaz especial para los casos que generalmente requieren la propagación de excepciones comprobadas: interface IUnsafeOut { T out() throws E; } interface IUnsafeOut { T out() throws E; } .

Se IOut tanto IOut como IUnsafeOut se eliminarán durante la migración a Java 8, sin embargo, no existe una coincidencia exacta para IUnsafeOut . Si las expresiones lambda podrían tratar con excepciones comprobadas como si estuvieran desmarcadas, podría ser posible usar simplemente como las siguientes en el constructor anterior:

 super(() -> DriverManager.getConnection(url), maxConnections); 

Eso se ve mucho más limpio. Veo que puedo reescribir la ObjectPool para aceptar nuestro IUnsafeOut , pero hasta donde yo sé, Java 8 aún no está terminado, por lo que podría haber algunos cambios como:

  • implementando algo similar a IUnsafeOut ? (para ser sincero, considero que es sucio; el sujeto debe elegir qué aceptar: ya sea de Factory o “fábrica insegura” que no puede tener firmas de métodos compatibles)
  • simplemente ignorando las excepciones marcadas en lambdas, por lo que no es necesario en IUnsafeOut sustitutos? (¿por qué no ?, por ejemplo, otro cambio importante: OpenJDK, que uso, javac ahora no requiere que las variables y los parámetros se declaren como final para ser capturados en una clase anónima [interfaz funcional] o expresión lambda)

Entonces, la pregunta es en general: ¿hay alguna manera de eludir las excepciones comprobadas en lambdas o se planea en el futuro hasta que finalmente se lance Java 8?


Actualización 1

Hm-mm, por lo que entiendo lo que tenemos actualmente, parece que no hay forma en este momento, a pesar de que el artículo referenciado data de 2010: Brian Goetz explica la transparencia de excepción en Java . Si nada ha cambiado mucho en Java 8, esto podría considerarse una respuesta. También Brian dice que la interface ExceptionalCallable (lo que mencioné como IUnsafeOut fuera de nuestro legado de código) es bastante inútil, y estoy de acuerdo con él.

¿Aún extraño algo más?

No estoy seguro de que realmente responda tu pregunta, pero ¿no podrías simplemente usar algo así?

 public final class SupplierUtils { private SupplierUtils() { } public static  Supplier wrap(Callable callable) { return () -> { try { return callable.call(); } catch (RuntimeException e) { throw e; } catch (Exception e) { throw new RuntimeException(e); } }; } } public class JdbcConnectionPool extends ObjectPool { public JdbcConnectionPool(int maxConnections, String url) { super(SupplierUtils.wrap(() -> DriverManager.getConnection(url)), maxConnections); } } 

En la lista de correo de lambda esto fue discutido a fondo . Como puede ver, Brian Goetz sugirió que la alternativa es escribir su propio combinador:

O podrías escribir tu propio combinador trivial:

 static Supplier exceptionWrappingSupplier(Supplier b) { return e -> { try { b.accept(e); } catch (Exception e) { throw new RuntimeException(e); } }; } 

Puede escribirlo una vez, en menos tiempo que el que tardó en escribir su correo electrónico original. Y de manera similar una vez para cada tipo de SAM que uses.

Prefiero que consideremos esto como “vaso lleno al 99%” en lugar de la alternativa. No todos los problemas requieren nuevas características del lenguaje como soluciones. (Sin mencionar que las nuevas características del lenguaje siempre causan nuevos problemas).

En esos días, la interfaz del consumidor se llamaba Block.

Creo que esto se corresponde con la respuesta de JB Nizet .

Más tarde, Brian explica por qué esto fue diseñado de esta manera (la razón del problema)

Sí, tendrías que proporcionar tus propios SAM excepcionales. Pero entonces la conversión lambda funcionaría bien con ellos.

El EG discutió el lenguaje adicional y el soporte bibliotecario para este problema, y ​​al final sintió que esto era una mala compensación de costo / beneficio.

Las soluciones basadas en bibliotecas causan una explosión de 2x en los tipos de SAM (excepcionales vs no), que interactúan mal con las explosiones combinatorias existentes para la especialización primitiva.

Las soluciones disponibles basadas en el lenguaje fueron perdedores de un compromiso de complejidad / valor. Aunque hay algunas soluciones alternativas que vamos a continuar explorando, aunque claramente no para 8 y probablemente tampoco para 9.

Mientras tanto, tienes las herramientas para hacer lo que quieras. Supongo que prefiere que proporcionemos esa última milla para usted (y, en segundo lugar, su solicitud es realmente una solicitud escasamente velada de “¿por qué no acaba de darse por vencido con las excepciones comprobadas?”), Pero creo que el estado actual permite haces tu trabajo hecho.

Septiembre de 2015:

Puedes usar ET para esto. ET es una pequeña biblioteca Java 8 para la conversión / traducción de excepciones.

Con ET puedes escribir:

 super(() -> et.withReturningTranslation(() -> DriverManager.getConnection(url)), maxConnections); 

Versión de línea múltiple:

 super(() -> { return et.withReturningTranslation(() -> DriverManager.getConnection(url)); }, maxConnections); 

Todo lo que debe hacer antes es crear una nueva instancia de ExceptionTranslator :

 ExceptionTranslator et = ET.newConfiguration().done(); 

Esta instancia es segura para subprocesos y varios componentes pueden compartirla. Puede configurar reglas de conversión de excepciones más específicas (por ejemplo, FooCheckedException -> BarRuntimeException ) si lo desea. Si no hay otras reglas disponibles, las excepciones comprobadas se convierten automáticamente a RuntimeException .

(Descargo de responsabilidad: soy el autor de ET)

¿Ha considerado utilizar una clase contenedora RuntimeException (no seleccionada) para pasar de contrabando la excepción original de la expresión lambda y luego devolver la excepción envuelta a su excepción comprobada original?

 class WrappedSqlException extends RuntimeException { static final long serialVersionUID = 20130808044800000L; public WrappedSqlException(SQLException cause) { super(cause); } public SQLException getSqlException() { return (SQLException) getCause(); } } public ConnectionPool(int maxConnections, String url) throws SQLException { try { super(() -> { try { return DriverManager.getConnection(url); } catch ( SQLException ex ) { throw new WrappedSqlException(ex); } }, maxConnections); } catch (WrappedSqlException wse) { throw wse.getSqlException(); } } 

Crear su propia clase única debería evitar cualquier posibilidad de confundir otra excepción no comprobada para la que envuelve dentro de su lambda, incluso si la excepción se serializa en algún lugar en la tubería antes de capturarla y volver a lanzarla.

Hmm … Lo único que veo que es un problema aquí es que estás haciendo esto dentro de un constructor con una llamada a super () que, por ley, debe ser la primera statement en tu constructor. ¿ try contar como una statement anterior? Tengo esto funcionando (sin el constructor) en mi propio código.

Desarrollamos un proyecto interno en mi empresa que nos ayudó con esto. Decidimos salir a bolsa hace dos meses.

Esto es lo que se nos ocurrió:

 @FunctionalInterface public interface ThrowingFunction { R apply(T arg) throws E; /** * @param  type * @param  checked exception * @return a function that accepts one argument and returns it as a value. */ static  ThrowingFunction identity() { return t -> t; } /** * @return a Function that returns the result of the given function as an Optional instance. * In case of a failure, empty Optional is returned */ static  Function> lifted(ThrowingFunction f) { Objects.requireNonNull(f); return f.lift(); } static  Function unchecked(ThrowingFunction f) { Objects.requireNonNull(f); return f.uncheck(); } default  ThrowingFunction compose(final ThrowingFunction before) { Objects.requireNonNull(before); return (V v) -> apply(before.apply(v)); } default  ThrowingFunction andThen(final ThrowingFunction after) { Objects.requireNonNull(after); return (T t) -> after.apply(apply(t)); } /** * @return a Function that returns the result as an Optional instance. In case of a failure, empty Optional is * returned */ default Function> lift() { return t -> { try { return Optional.of(apply(t)); } catch (Throwable e) { return Optional.empty(); } }; } /** * @return a new Function instance which wraps thrown checked exception instance into a RuntimeException */ default Function uncheck() { return t -> { try { return apply(t); } catch (final Throwable e) { throw new WrappedException(e); } }; } 

}

https://github.com/TouK/ThrowingFunction/

Envolver la excepción de la manera descrita no funciona. Lo intenté y aún recibo errores de comstackción, que en realidad están de acuerdo con la especificación: la expresión lambda arroja la excepción que es incompatible con el tipo de destino del argumento de método: Callable; call () no lo arroja así que no puedo pasar la expresión lambda como un invocable.

Entonces, básicamente, no hay solución: estamos atrapados con la escritura repetitiva. Lo único que podemos hacer es express nuestra opinión de que esto debe solucionarse. Creo que la especificación no debería descartar ciegamente un tipo de destino basado en excepciones lanzadas incompatibles: posteriormente debería comprobar si la excepción lanzada incompatible se detecta o se declara como throws en el ámbito de invocación. Y para las expresiones lambda que no están en línea, propongo que podemos marcarlas como lanzando silenciosamente la excepción marcada (silenciosa en el sentido de que el comstackdor no debería verificar, pero el tiempo de ejecución todavía debería capturarse). marquemos aquellos con => en lugar de -> Sé que este no es un sitio de discusión, pero ya que esta ES la única solución a la pregunta, ¡déjese oír y cambiemos esta especificación!

Paguro proporciona interfaces funcionales que envuelven las excepciones marcadas . Empecé a trabajar en ello unos meses después de que me hiciste tu pregunta, ¡así que probablemente fuiste parte de la inspiración para ello!

Notarás que solo hay 4 interfaces funcionales en Paguro frente a las 43 interfaces incluidas en Java 8. Esto se debe a que Paguro prefiere los generics a los primitivos.

Paguro tiene transformaciones de paso único integradas en sus colecciones inmutables (copiadas de Clojure). Estas transformaciones son más o menos equivalentes a los transductores Clojure o las secuencias Java 8, pero aceptan interfaces funcionales que envuelven las excepciones comprobadas. Ver: las diferencias entre las secuencias de Paguro y Java 8 .

Puedes lanzar desde tus lambdas, solo tienen que ser declaradas “a tu manera” (lo que lamentablemente no las vuelve a usar en el código JDK estándar, pero bueno, hacemos lo que podemos).

 @FunctionalInterface public interface SupplierIOException { MyClass get() throws IOException; } 

O la versión más genérica -ized:

 public interface ThrowingSupplier { T get() throws E; } 

ref aquí . También se menciona el uso de “sneakyThrow” para no declarar las excepciones marcadas, pero aún así arrojarlas. Me duele un poco la cabeza, tal vez sea una opción.