Exceções em expressões do Java 8 Lambda
1. Visão geral
No Java 8, o Lambda Expressions começou a facilitar a programação funcional, fornecendo uma maneira concisa de expressar o comportamento. No entanto, osFunctional Interfaces fornecidos pelo JDK não lidam com exceções muito bem - e o código se torna prolixo e complicado quando se trata de manipulá-las.
Neste artigo, vamos explorar algumas maneiras de lidar com exceções ao escrever expressões lambda.
2. Tratamento de exceções não verificadas
Primeiro, vamos entender o problema com um exemplo.
Temos umList<Integer>e queremos dividir uma constante, digamos 50 com cada elemento desta lista e imprimir os resultados:
List integers = Arrays.asList(3, 9, 7, 6, 10, 20);
integers.forEach(i -> System.out.println(50 / i));
Esta expressão funciona, mas há um problema. Se algum dos elementos da lista for0, obteremos umArithmeticException: / by zero. Vamos consertar isso usando um blocotry-catch tradicional de modo que registremos qualquer exceção e continuemos a execução para os próximos 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());
}
});
O uso detry-catch resolve o problema, mas a concisão de aLambda Expression é perdida e não é mais uma função pequena como deveria ser.
Para lidar com esse problema, podemos escrevera lambda wrapper for the lambda function. Vejamos o código para ver como 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)));
Inicialmente, escrevemos um método wrapper que será responsável pelo tratamento da exceção e passamos a expressão lambda como parâmetro para esse método.
O método wrapper funciona conforme o esperado, mas você pode argumentar que basicamente remove o blocotry-catch da expressão lambda e o move para outro método e não reduz o número real de linhas de código sendo escrito.
Isso é verdade neste caso em que o wrapper é específico para um caso de uso específico, mas podemos usar genéricos para aprimorar esse método e usá-lo em vários outros cenários:
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 iteração do nosso método wrapper leva dois argumentos, a expressão lambda e o tipo deException a ser capturado. Este wrapper lambda é capaz de manipular todos os tipos de dados, não apenasIntegers, e capturar qualquer tipo específico de exceção e não a superclasseException.
Além disso, observe que alteramos o nome do método delambdaWrapper paraconsumerWrapper. É porque este método só lida com expressões lambda paraFunctional Interface do tipoConsumer. Podemos escrever métodos de wrapper semelhantes para outras interfaces funcionais comoFunction,BiFunction,BiConsumere assim por diante.
3. Tratamento de exceções verificadas
Vamos considerar o exemplo da seção anterior, mas em vez de dividir e imprimir os inteiros no console, queremos gravá-los em um arquivo. Esta operação de gravação em um arquivo geraIOException.
static void writeToFile(Integer integer) throws IOException {
// logic to write to file which throws IOException
}
List integers = Arrays.asList(3, 9, 7, 0, 10, 20);
integers.forEach(i -> writeToFile(i));
Na compilação, obtemos o seguinte erro.
java.lang.Error: Unresolved compilation problem: Unhandled exception type IOException
ComoIOException é uma exceção verificada, ela deve ser tratada. Agora, existem duas opções: podemos lançar a exceção e manipulá-la em outro lugar ou dentro do método que possui a expressão lambda. Vamos dar uma olhada em cada um deles um por um.
3.1. Lançar exceção verificada de expressões lambda
Vamos lançar a exceção do método em que a expressão lambda é escrita, neste caso, omain:
public static void main(String[] args) throws IOException {
List integers = Arrays.asList(3, 9, 7, 0, 10, 20);
integers.forEach(i -> writeToFile(i));
}
Ainda assim, durante a compilação, obtemos o mesmo erro deIOException não tratado. Isso ocorre porque as expressões lambda são semelhantes às Classes Internas Anônimas. Nesse caso, a expressão lambda é uma implementação do métodoaccept(T t) da interfaceConsumer<T>.
Lançar a exceção demain não faz nada e, uma vez que o método na interface pai não lança nenhuma exceção, ele não pode em sua implementação:
Consumer consumer = new Consumer() {
@Override
public void accept(Integer integer) throws Exception {
writeToFile(integer);
}
};
O código acima não compila porque a implementação do métodoaccept não pode lançar nenhuma exceção.
A maneira mais direta seria usar umtry-catche envolver a exceção verificada em uma exceção não verificada e relançar:
List integers = Arrays.asList(3, 9, 7, 0, 10, 20);
integers.forEach(i -> {
try {
writeToFile(i);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
Essa abordagem permite que o código seja compilado e executado, mas tem o mesmo problema do exemplo no caso de exceções não verificadas na seção anterior.
Como queremos apenas lançar a exceção, precisamos escrever nossa própria Interface Funcional do Consumidor, que pode lançar uma exceção e, em seguida, um método wrapper usando-a. Vamos chamá-lo deThrowingConsumer:
@FunctionalInterface
public interface ThrowingConsumer {
void accept(T t) throws E;
}
static Consumer throwingConsumerWrapper(
ThrowingConsumer throwingConsumer) {
return i -> {
try {
throwingConsumer.accept(i);
} catch (Exception ex) {
throw new RuntimeException(ex);
}
};
}
Agora podemos escrever nossa expressão lambda, que pode gerar exceções sem perder a concisão.
List integers = Arrays.asList(3, 9, 7, 0, 10, 20);
integers.forEach(throwingConsumerWrapper(i -> writeToFile(i)));
3.2. Lidando com uma exceção verificada na expressão Lambda
Nesta seção final. modificaremos o wrapper para lidar com exceções verificadas. Como nossa interfaceThrowingConsumer usa genéricos, podemos lidar com qualquer exceção 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);
}
}
};
}
Podemos usar este wrapper em nosso exemplo para lidar apenas comIOExceptione lançar qualquer outra exceção verificada, envolvendo-os em uma exceção não verificada:
List integers = Arrays.asList(3, 9, 7, 0, 10, 20);
integers.forEach(handlingConsumerWrapper(
i -> writeToFile(i), IOException.class));
Semelhante ao caso de exceções não verificadas, jogando irmãos para outras interfaces funcionais comoThowingFunction,ThrowingBiFunction,ThrowingBiConsumer etc. pode ser escrito junto com seus métodos de wrapper correspondentes.
4. Conclusão
Neste artigo, abordamos como lidar com uma exceção específica em expressões lambda sem perder a concisão usando métodos de wrapper. Também aprendemos a escrever alternativas de lançamento para as Interfaces Funcionais presentes no JDK para lançar uma exceção verificada envolvendo-as em uma exceção não verificada ou para manipulá-las.
Outra forma seriaexplore the sneaky-throws hack.
O código-fonte completo da Interface Funcional e métodos de wrapper pode ser baixado deheree classes de teste dehere, over on Github.
Se você estiver procurando por soluções de trabalho prontas para usar, vale a pena conferir o projetoThrowingFunction.