1. Información general
En Java 8, Lambda Expressions comenzó a facilitar la programación funcional al proporcionar una forma concisa de expresar el comportamiento. Sin embargo, las interfaces funcionales proporcionadas por el JDK no manejan las excepciones muy bien, y el código se vuelve detallado y engorroso cuando se trata de manejarlas.
En este artículo, exploraremos algunas formas de lidiar con las excepciones al escribir expresiones lambda.
2. Manejo de excepciones no marcadas
Primero, entendamos el problema con un ejemplo.
Tenemos una lista y queremos dividir una constante, digamos 50 con cada elemento de esta lista e imprimir los resultados:
List integers = Arrays.asList(3, 9, 7, 6, 10, 20); integers.forEach(i -> System.out.println(50 / i));
Esta expresión funciona, pero hay un problema. Si alguno de los elementos de la lista es 0 , obtenemos una ArithmeticException: / por cero . Arreglemos eso usando un bloque tradicional try-catch de modo que registremos cualquier excepción y continuemos la ejecución para los siguientes elementos:
List integers = Arrays.asList(3, 9, 7, 0, 10, 20); integers.forEach(i -> { try { System.out.println(50 / i); } catch (ArithmeticException e) { System.err.println( "Arithmetic Exception occured : " + e.getMessage()); } });
El uso de try-catch resuelve el problema, pero se pierde la concisión de una expresión Lambda y ya no es una función pequeña como se supone que es.
Para solucionar este problema, podemos escribir una envoltura lambda para la función lambda . Veamos el código para ver cómo funciona:
static Consumer lambdaWrapper(Consumer consumer) { return i -> { try { consumer.accept(i); } catch (ArithmeticException e) { System.err.println( "Arithmetic Exception occured : " + e.getMessage()); } }; }
List integers = Arrays.asList(3, 9, 7, 0, 10, 20); integers.forEach(lambdaWrapper(i -> System.out.println(50 / i)));
Al principio, escribimos un método contenedor que será responsable de manejar la excepción y luego pasamos la expresión lambda como parámetro a este método.
El método de envoltura funciona como se esperaba, pero puede argumentar que básicamente está eliminando el bloque try-catch de la expresión lambda y moviéndolo a otro método y no reduce la cantidad real de líneas de código que se están escribiendo.
Esto es cierto en este caso donde el contenedor es específico para un caso de uso particular, pero podemos hacer uso de genéricos para mejorar este método y usarlo para una variedad de otros escenarios:
static Consumer consumerWrapper(Consumer consumer, Class clazz) { return i -> { try { consumer.accept(i); } catch (Exception ex) { try { E exCast = clazz.cast(ex); System.err.println( "Exception occured : " + exCast.getMessage()); } catch (ClassCastException ccEx) { throw ex; } } }; }
List integers = Arrays.asList(3, 9, 7, 0, 10, 20); integers.forEach( consumerWrapper( i -> System.out.println(50 / i), ArithmeticException.class));
Como podemos ver, esta iteración de nuestro método contenedor toma dos argumentos, la expresión lambda y el tipo de excepción que se va a capturar. Esta envoltura lambda es capaz de manejar todos los tipos de datos, no solo enteros , y detectar cualquier tipo específico de excepción y no la excepción de superclase .
Además, observe que hemos cambiado el nombre del método de lambdaWrapper a consumerWrapper . Es porque este método solo maneja expresiones lambda para Interfaz funcional de tipo Consumer . Podemos escribir métodos de envoltura similares para otras interfaces funcionales como Function , BiFunction , BiConsumer , etc.
3. Manejo de excepciones marcadas
Modifiquemos el ejemplo de la sección anterior y en lugar de imprimir en la consola, escribamos en un archivo.
static void writeToFile(Integer integer) throws IOException { // logic to write to file which throws IOException }
Tenga en cuenta que el método anterior puede generar la IOException.
List integers = Arrays.asList(3, 9, 7, 0, 10, 20); integers.forEach(i -> writeToFile(i));
En la compilación, obtenemos el error:
java.lang.Error: Unresolved compilation problem: Unhandled exception type IOException
Debido a que IOException es una excepción marcada, debemos manejarla explícitamente . Tenemos dos opciones.
Primero, podemos simplemente lanzar la excepción fuera de nuestro método y ocuparnos de ella en otro lugar.
Alternativamente, podemos manejarlo dentro del método que usa una expresión lambda.
Exploremos ambas opciones.
3.1. Lanzar una excepción marcada de expresiones Lambda
Veamos qué sucede cuando declaramos la IOException en el método principal :
public static void main(String[] args) throws IOException { List integers = Arrays.asList(3, 9, 7, 0, 10, 20); integers.forEach(i -> writeToFile(i)); }
Aún así, obtenemos el mismo error de IOException no controlada durante la compilación .
java.lang.Error: Unresolved compilation problem: Unhandled exception type IOException
Esto se debe a que las expresiones lambda son similares a las clases internas anónimas.
En nuestro caso, el método writeToFile es la implementación de la interfaz funcional del consumidor .
Echemos un vistazo a la definición del consumidor :
@FunctionalInterface public interface Consumer { void accept(T t); }
Como podemos ver, el método accept no declara ninguna excepción marcada. Es por eso que writeToFile no puede lanzar la IOException.
La forma más sencilla sería usar un bloque try-catch , envolver la excepción marcada en una excepción no marcada y volver a lanzarla:
List integers = Arrays.asList(3, 9, 7, 0, 10, 20); integers.forEach(i -> { try { writeToFile(i); } catch (IOException e) { throw new RuntimeException(e); } });
Esto hace que el código se compile y se ejecute. Sin embargo, este enfoque presenta el mismo problema que ya discutimos en la sección anterior: es detallado y engorroso.
Podemos ser mejores que eso.
Creemos una interfaz funcional personalizada con un único método de aceptación que arroja una excepción.
@FunctionalInterface public interface ThrowingConsumer { void accept(T t) throws E; }
Y ahora, implementemos un método contenedor que pueda volver a generar la excepción:
static Consumer throwingConsumerWrapper( ThrowingConsumer throwingConsumer) { return i -> { try { throwingConsumer.accept(i); } catch (Exception ex) { throw new RuntimeException(ex); } }; }
Finalmente, podemos simplificar la forma en que usamos el método writeToFile :
List integers = Arrays.asList(3, 9, 7, 0, 10, 20); integers.forEach(throwingConsumerWrapper(i -> writeToFile(i)));
Esto sigue siendo una especie de solución, pero el resultado final parece bastante limpio y definitivamente es más fácil de mantener .
Tanto ThrowingConsumer como throwingConsumerWrapper son genéricos y pueden reutilizarse fácilmente en diferentes lugares de nuestra aplicación.
3.2. Manejo de una excepción marcada en la expresión Lambda
En esta sección final, modificaremos el contenedor para manejar las excepciones marcadas.
Dado que nuestra interfaz ThrowingConsumer usa genéricos, podemos manejar fácilmente cualquier excepción específica.
static Consumer handlingConsumerWrapper( ThrowingConsumer throwingConsumer, Class exceptionClass) { return i -> { try { throwingConsumer.accept(i); } catch (Exception ex) { try { E exCast = exceptionClass.cast(ex); System.err.println( "Exception occured : " + exCast.getMessage()); } catch (ClassCastException ccEx) { throw new RuntimeException(ex); } } }; }
Veamos cómo usarlo en la práctica:
List integers = Arrays.asList(3, 9, 7, 0, 10, 20); integers.forEach(handlingConsumerWrapper( i -> writeToFile(i), IOException.class));
Tenga en cuenta que el código anterior solo maneja IOException, mientras que cualquier otro tipo de excepción se vuelve a generar como RuntimeException .
4. Conclusión
En este artículo, mostramos cómo manejar una excepción específica en una expresión lambda sin perder la concisión con la ayuda de métodos de envoltura. También aprendimos cómo escribir alternativas de lanzamiento para las interfaces funcionales presentes en JDK para lanzar o manejar una excepción marcada.
Otra forma sería explorar el truco de los tiros furtivos.
El código fuente completo de la interfaz funcional y los métodos de envoltura se pueden descargar desde aquí y las clases de prueba desde aquí, en Github.
Si está buscando soluciones de trabajo listas para usar, vale la pena echarle un vistazo al proyecto ThrowingFunction.