Tratamento de exceções em Java Streams

Ao longo do meu tempo trabalhando com empresas que estão migrando do Java 7 para o Java 8, vi muitas pessoas entusiasmadas com o uso de novos recursos interessantes, como fluxos e funções de ordem superior. O Java 8 elimina muitos códigos clichê e torna o código muito menos prolixo.

Mas não fazer o tratamento de exceções corretamente pode acabar fazendo com que tenhamos lambdas inchados que vão contra o propósito de ter um código limpo.

Aqui, gostaria de discutir algumas coisas que aprendi ao longo do tempo sobre como lidar com exceções em lambdas. Lembre-se de que sempre há várias abordagens para coisas como essa, então aceite isso com uma pitada de sal. Se você acha que existem maneiras melhores de fazer as coisas abaixo, comente ai em baixo.

Vamos dar um exemplo de uso de streams: Você recebe uma lista de strings e deseja convertê-los todos em inteiros. Para conseguir isso, podemos fazer algo simples como isto:

List<String> integers = Arrays.asList("44", "373", "145");
integers.forEach(str -> System.out.println(Integer.parseInt(str)));

O trecho acima funcionará perfeitamente, mas o que acontece se modificarmos a entrada para conter uma string ilegal, digamos “xyz”. O método parseInt() lançará um NumberFormatException, que é um tipo de exceção não verificada.

Uma solução ingênua, tipicamente vista, é envolver a chamada em um bloco try / catch e tratá-la. Isso ficaria assim:

List<String> integers = Arrays.asList("44", "373", "xyz", "145");
integers.forEach(str -> {
    try {
        System.out.println(Integer.parseInt(str));
    }catch (NumberFormatException ex) {
        System.err.println("Can't format this string");
    }
}
);

Embora funcione, isso anula o propósito de escrever pequenos lambdas para tornar o código legível e menos prolixo. A solução que vem à mente é envolver o lambda em torno de outro lambda que faz o tratamento de exceção para você, mas que basicamente é apenas mover o código de tratamento de exceção para outro lugar:

static Consumer<String> exceptionHandledConsumer(Consumer<String> unhandledConsumer) {
    return obj -> {
        try {
            unhandledConsumer.accept(obj);
        } catch (NumberFormatException e) {
            System.err.println(
                    "Can't format this string");
        }
    };
}
public static void main(String[] args) {
    List<String> integers = Arrays.asList("44", "xyz", "145");
    integers.forEach(exceptionHandledConsumer(str -> System.out.println(Integer.parseInt(str))));
}

A solução acima pode ser feita muito, muito melhor usando genéricos. Vamos construir um consumidor genérico com tratamento de exceção que possa tratar de todos os tipos de exceções. Seremos, então, capazes de usá-lo para muitos casos de uso diferentes em nosso aplicativo.

Podemos fazer uso do código acima para construir nossa implementação genérica. Não vou entrar em detalhes de como os genéricos funcionam, mas uma boa implementação seria assim:

static <Target, ExObj extends Exception> Consumer<Target> handledConsumer(Consumer<Target> targetConsumer, Class<ExObj> exceptionClazz) {
    return obj -> {
        try {
            targetConsumer.accept(obj);
        } catch (Exception ex) {
            try {
                ExObj exCast = exceptionClazz.cast(ex);
                System.err.println(
                        "Exception occured : " + exCast.getMessage());
            } catch (ClassCastException ccEx) {
                throw ex;
            }
        }
    };
}

Como você pode ver, esse novo consumidor não está vinculado a nenhum tipo específico de objeto que consome e aceita o tipo de exceção que seu código pode lançar como parâmetro. Agora podemos simplesmente usar o handledConsumer método para construir nossos consumidores. O código para analisar nossa lista de Strings em Inteiros agora será este:

List<String> integers = Arrays.asList("44", "373", "xyz", "145");
integers.forEach(
        handledConsumer(str -> System.out.println(Integer.parseInt(str)), 
        NumberFormatException.class));

Se você tiver um bloco de código diferente que pode lançar uma exceção diferente, você pode simplesmente reutilizar o método acima. Por exemplo, o código abaixo cuida ArithmeticException devido a uma divisão por zero.

List<Integer> ints = Arrays.asList(5, 10, 0, 15, 20, 30, 0, 9);
ints.forEach(
        handledConsumer(
                i -> System.out.println(1000 / i),
                ArithmeticException.class));

Exceções verificadas

Para dar uma olhada nas exceções verificadas, usaremos o exemplo do Thread.sleep() método, que lança um InterruptedException.

Digamos que temos uma lista de inteiros contendo o tempo em milissegundos pelo qual desejamos que o thread seja suspenso. Tentando fazer isso:

public static void sleeper() {
    List<Integer> list = Arrays.asList(5, 4, 3, 2, 1);
    list.forEach(i -> Thread.sleep(i));
}

apresentará um erro de compilação, pois a exceção verificada não foi tratada. Adicionar throws à assinatura do método de sleeper() também não resolverá o problema:

public static void sleeper() throws InterruptedException {
    List<Integer> list = Arrays.asList(5, 4, 3, 2, 1);
    list.forEach(i -> Thread.sleep(i));
}

Isso ocorre porque lambdas são, depois de todas as implementações de classes anônimas. Precisamos lidar com a exceção dentro da implementação da interface que o lambda está implementando. O método pai de chamada não tem nada a ver com isso.

Mais uma vez, a solução ingênua é adicionar um bloco try-catch dentro do lambda, que obviamente funciona:

List<Integer> list = Arrays.asList(5, 4, 3, 2, 1);
    list.forEach(i -> {
        try {
            Thread.sleep(i);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });
}

Vamos nos inspirar em nossa implementação genérica para lidar com exceções não verificadas. Podemos estender a Consumer interface para lidar com as exceções verificadas. Vamos ver como.

Etapa 1: construir uma nova interface que tratará das exceções verificadas. Vamos chamá-lo HandlingConsumer:

@FunctionalInterface
public interface HandlingConsumer<Target, ExObj extends Exception> {
    void accept(Target target) throws ExObj;
}

Etapa 2: adicione um método estático à interface para construir a nova interface funcional que acabamos de escrever (caso tenha esquecido, você pode escrever métodos estáticos dentro de interfaces desde o Java 8) e certifique-se de converter a exceção marcada em um RuntimeException. Por quê? Porque isso nos permite lidar com a exceção nos métodos de chamada e libera o lambda para fazer seu trabalho real:

@FunctionalInterface
public interface HandlingConsumer<Target, ExObj extends Exception> {
    void accept(Target target) throws ExObj;
    static <Target> Consumer<Target> handlingConsumerBuilder(
            HandlingConsumer<Target, Exception> handlingConsumer) {
        return obj -> {
            try {
                handlingConsumer.accept(obj);
            } catch (Exception ex) {
                throw new RuntimeException(ex);
            }
        };
    }
}

Nosso código para realizar a suspensão do thread agora pode ser simplesmente este:

List<Integer> list = Arrays.asList(5, 4, 3, 2, 1);
list.forEach(handlingConsumerBuilder(i->Thread.sleep(i)));

O método handlingConsumerBuilder pode ser reutilizado para quaisquer lambdas que precisem lidar com exceções verificadas, mantendo a sanidade de nossos lambdas e reduzindo o tamanho geral do código.

Espero que você ache essas técnicas úteis para tornar seu código mais legível e menos prolixo, e também permita que você trate suas exceções corretamente.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.