tipos - Java 8: control obligatorio de excepciones comprobadas en expresiones lambda. ¿Por qué obligatorio, no opcional?
tipos de excepciones en java (8)
¿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 declaración en tu constructor. ¿ try
contar como una declaración anterior? Tengo esto funcionando (sin el constructor) en mi propio código.
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<Connection> {
public ConnectionPool(int maxConnections, String url) {
super(new Factory<Connection>() {
@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<Connection> {
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<T>
que es equivalente ajava.lang.functions.Factory
; - y una interfaz especial para los casos que generalmente requieren la propagación de excepciones comprobadas:
interface IUnsafeOut<T, E extends Throwable> { T out() throws E; }
interface IUnsafeOut<T, E extends Throwable> { T out() throws E; }
.
Se IOut<T>
tanto IOut<T>
como IUnsafeOut<T>
se eliminarán durante la migración a Java 8, sin embargo, no existe una coincidencia exacta para IUnsafeOut<T, E>
. 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<T>
, 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<T, E>
? (para ser sincero, considero que es sucio; el sujeto debe elegir qué aceptar: ya sea deFactory
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<T, E>
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 comofinal
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<V, E extends Exception>
(lo que mencioné como IUnsafeOut<T, E extends Throwable>
fuera de nuestro legado de código) es bastante inútil, y estoy de acuerdo con él.
¿Aún extraño algo más?
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<T,R,E extends Throwable> {
R apply(T arg) throws E;
/**
* @param <T> type
* @param <E> checked exception
* @return a function that accepts one argument and returns it as a value.
*/
static <T, E extends Exception> ThrowingFunction<T, T, E> 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 <T, R, E extends Exception> Function<T, Optional<R>> lifted(ThrowingFunction<T, R, E> f) {
Objects.requireNonNull(f);
return f.lift();
}
static <T, R, E extends Exception> Function<T, R> unchecked(ThrowingFunction<T, R, E> f) {
Objects.requireNonNull(f);
return f.uncheck();
}
default <V> ThrowingFunction<V, R, E> compose(final ThrowingFunction<? super V, ? extends T, E> before) {
Objects.requireNonNull(before);
return (V v) -> apply(before.apply(v));
}
default <V> ThrowingFunction<T, V, E> andThen(final ThrowingFunction<? super R, ? extends V, E> 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<T, Optional<R>> 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<T, R> uncheck() {
return t -> {
try {
return apply(t);
} catch (final Throwable e) {
throw new WrappedException(e);
}
};
}
}
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<T> Supplier<T> exceptionWrappingSupplier(Supplier<T> 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.
Envolver la excepción de la manera descrita no funciona. Lo intenté y aún recibo errores de compilació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 expresar 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 compilador 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!
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 <T> Supplier<T> wrap(Callable<T> callable) {
return () -> {
try {
return callable.call();
}
catch (RuntimeException e) {
throw e;
}
catch (Exception e) {
throw new RuntimeException(e);
}
};
}
}
public class JdbcConnectionPool extends ObjectPool<Connection> {
public JdbcConnectionPool(int maxConnections, String url) {
super(SupplierUtils.wrap(() -> DriverManager.getConnection(url)), maxConnections);
}
}
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, E extends Exception> {
T get() throws E;
}
ref here . 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.
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 genéricos 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 .
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)