Quando você quer usar um método que lança uma exceção checked, você precisa fazer algo extra se quiser chamá-lo em um lambda.

O Stream API e o lambda’s tiveram uma grande melhoria em Java desde a versão 8. A partir daí, pudemos trabalhar com um estilo de sintaxe mais funcional. Agora, depois de alguns anos trabalhando com essas construções de código, um dos maiores problemas que permanecem é como lidar com as exceções verificadas dentro de um lambda.

Como todos vocês provavelmente sabem, não é possível chamar um método que lança diretamente uma exceção checada de um lambda. De alguma forma, precisamos capturar a exceção para compilar o código. Naturalmente, podemos fazer um simples try-catch dentro do lambda a exceção de um RuntimeException, como mostrado no primeiro exemplo, mas acho que todos podemos concordar que este não é o melhor caminho a percorrer.

myList.stream()
  .map(item -> {
    try {
      return doSomething(item);
    } catch (MyException e) {
      throw new RuntimeException(e);
    }
  })
  .forEach(System.out::println);

A maioria de nós está ciente de que os lambdas de blocos são desajeitados e menos legíveis. Eles devem ser evitados tanto quanto possível, na minha opinião. Se precisarmos fazer mais do que uma única linha, podemos extrair o corpo da função em um método separado e simplesmente chamar o novo método. Uma maneira melhor e mais legível de resolver esse problema é encapsular a chamada em um método antigo e simples que faz o try-catch e chamar esse método de dentro do seu lambda.

myList.stream()
  .map(this::trySomething)
  .forEach(System.out::println);

private Item trySomething(Item item) {
  try {
    return doSomething(item);
  } catch (MyException e) {
    throw new RuntimeException(e);
  }
}

Essa solução é pelo menos um pouco mais legível e separamos nossas preocupações. Se você realmente deseja capturar a exceção e fazer algo específico e não simplesmente a exceção de umaRuntimeException`, essa pode ser uma solução possível e legível para você.

Exceção de tempo de execução

Em muitos casos, você verá que as pessoas usam esses tipos de soluções para reempacotar a exceção em uma RuntimeException ou uma implementação mais específica de uma exceção não verificada. Ao fazer isso, o método pode ser chamado dentro de um lambda e ser usado em funções de ordem superior.

Eu posso me relacionar um pouco com essa prática, porque eu pessoalmente não vejo muito valor nas exceções verificadas em geral, mas essa é uma outra discussão que eu não vou começar aqui. Se você quiser quebrar todas as chamadas em um lambda que tenha um check-in de um RuntimeException, você verá que você repete o mesmo padrão. Para evitar reescrever o mesmo código várias vezes, por que não abstraê-lo em uma função de utilidade? Dessa forma, você só precisa escrever uma vez e ligar toda vez que precisar.

Para fazer isso, primeiro você precisa escrever sua própria versão da interface funcional para uma função. Só que desta vez, você precisa definir que a função pode lançar uma exceção.

@FunctionalInterface
public interface CheckedFunction<T,R> {
    R apply(T t) throws Exception;
}

Agora, você está pronto para escrever sua própria função de utilitário geral que aceita um CheckedFunction como você acabou de descrever na interface. Você pode manipular o try-catch nessa função de utilitário e encapsular a exceção original em uma RuntimeException (ou alguma outra variante não verificada). Eu sei que agora terminamos com um bloco lambda feio aqui e você pode abstrair o corpo disso. Escolha por si mesmo se isso vale o esforço para este único utilitário.

public static <T,R> Function<T,R> wrap(CheckedFunction<T,R> checkedFunction) {
  return t -> {
    try {
      return checkedFunction.apply(t);
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  };
}

Com uma simples importação estática, agora você pode envolver o lambda que pode lançar uma exceção com sua nova função de utilitário. Deste ponto em diante, tudo vai funcionar novamente.

myList.stream()
       .map(wrap(item -> doSomething(item)))
       .forEach(System.out::println);

O único problema que resta é que, quando ocorre uma exceção, o processamento do seu fluxo é interrompido imediatamente. Se isso não é problema para você, então vá em frente. Eu posso imaginar, no entanto, que a terminação direta não é ideal em muitas situações.

Either

Ao trabalhar com fluxos, provavelmente não queremos parar de processar o fluxo se ocorrer uma exceção. Se o seu fluxo contiver uma grande quantidade de itens que precisam ser processados, você deseja que o fluxo termine quando, por exemplo, o segundo item lança uma exceção? Provavelmente não.

Vamos voltar nossa maneira de pensar. Por que não considerar “a situação excepcional” tanto quanto um resultado possível, como desejaríamos para um resultado “bem-sucedido”? Vamos considerá-lo como dados, continuar processando o fluxo e decidir depois o que fazer com ele. Podemos fazer isso, mas, para tornar isso possível, precisamos introduzir um novo tipo - o tipo Either.

O tipo E é um tipo comum em linguagens funcionais e não (ainda) parte do Java. Semelhante ao tipo Opcional em Java, um Either é um wrapper genérico com duas possibilidades. Pode ser uma esquerda ou uma direita, mas nunca ambas. Tanto a esquerda como a direita podem ser de qualquer tipo. Por exemplo, se tivermos um valor Either, esse valor poderá conter algo do tipo String ou do tipo Integer, Either<String,Integer>.

Se usarmos esse princípio para o tratamento de exceções, podemos dizer que nosso tipo Either possui uma Exception ou um valor. Por conveniência, normalmente, a esquerda é a Exceção e a direita é o valor de sucesso. Você pode se lembrar disso pensando no lado direito não apenas do lado direito, mas também como sinônimo de “bom”, “ok” etc.

Abaixo, você verá uma implementação básica do tipo Either. Neste caso, usei o tipo Optional quando tentamos obter a esquerda ou a direita porque:

public class Either<L, R> {

    private final L left;
    private final R right;

    private Either(L left, R right) {
        this.left = left;
        this.right = right;
    }

    public static <L,R> Either<L,R> Left( L value) {
        return new Either(value, null);
    }

    public static <L,R> Either<L,R> Right( R value) {
        return new Either(null, value);
    }

    public Optional<L> getLeft() {
        return Optional.ofNullable(left);
    }

    public Optional<R> getRight() {
        return Optional.ofNullable(right);
    }

    public boolean isLeft() {
        return left != null;
    }

    public boolean isRight() {
        return right != null;
    }

    public <T> Optional<T> mapLeft(Function<? super L, T> mapper) {
        if (isLeft()) {
            return Optional.of(mapper.apply(left));
        }
        return Optional.empty();
    }

    public <T> Optional<T> mapRight(Function<? super R, T> mapper) {
        if (isRight()) {
            return Optional.of(mapper.apply(right));
        }
        return Optional.empty();
    }

    public String toString() {
        if (isLeft()) {
            return "Left(" + left +")";
        }
        return "Right(" + right +")";
    }
}

Agora você pode fazer suas próprias funções retornarem um Either em vez de lançar uma Exception. Mas isso não ajuda se você quiser usar métodos existentes que lançam uma Exception verificada dentro de um lambda certo? Portanto, temos que adicionar uma pequena função de utilidade ao Either tipo o que descrevi acima.

public static <T,R> Function<T, Either> lift(CheckedFunction<T,R> function) {
  return t -> {
    try {
      return Either.Right(function.apply(t));
    } catch (Exception ex) {
      return Either.Left(ex);
    }
  };
}

Ao adicionar este método de elevação estática ao Either, agora podemos simplesmente “elevar” uma função que lança uma exceção verificada e permite que ela retorne um Either. Se tomarmos o problema original, agora acabamos com um fluxo de Eithers em vez de uma possível RuntimeException. Isso pode explodir todo o meu Stream.

myList.stream()
       .map(Either.lift(item -> doSomething(item)))
       .forEach(System.out::println);

Isso significa simplesmente que recuperamos o controle. Usando a função de filtro na APU Stream, podemos simplesmente filtrar as instâncias à esquerda e, por exemplo, registrá-las. Você também pode filtrar as instâncias certas e simplesmente ignorar os casos excepcionais. De qualquer forma, você está de volta ao controle novamente e seu fluxo não será encerrado instantaneamente quando ocorrer uma possível RuntimeException.

Porque Either é um wrapper genérico, ele pode ser usado para qualquer tipo, não apenas para manipulação de exceção. Isso nos dá a oportunidade de fazer mais do que apenas envolver a Exception na parte esquerda de um dos dois. A questão que agora podemos ter, é que, se o Either só detém a exceção empacotada e não podemos fazer uma nova tentativa porque perdemos o valor original. Usando a habilidade do Either para segurar qualquer coisa, podemos armazenar a exceção e o valor dentro de uma esquerda. Para fazer isso, simplesmente fazemos uma segunda função de elevação estática como essa.

public static <T,R> Function<T, Either> liftWithValue(CheckedFunction<T,R> function) {
  return t -> {
    try {
      return Either.Right(function.apply(t));
    } catch (Exception ex) {
      return Either.Left(Pair.of(ex,t));
    }
  };
}

Você vê que neste liftWithValue função dentro do tipo Pair é usado para emparelhar a exceção e o valor original à esquerda de um dos dois Either. Agora, temos todas as informações de que possivelmente precisamos se algo der errado, em vez de apenas ter a Exception.

O tipo Pair usado aqui é outro tipo genérico que pode ser encontrado na biblioteca lang do Apache Commons, ou você pode simplesmente implementar o seu próprio. De qualquer forma, é apenas um tipo que pode conter dois valores.

public class Pair<F,S> {

    public final F fst;
    public final S snd;

    private Pair(F fst, S snd) {
        this.fst = fst;
        this.snd = snd;
    }

    public static <F,S> Pair<F,S> of(F fst, S snd) {
        return new Pair<>(fst,snd);
    }
}

Com o uso do liftWithValue, agora você tem toda a flexibilidade e controle para usar métodos que possam gerar uma Exception dentro de um lambda. Quando o outro Either é um direito, sabemos que a função foi aplicada corretamente e podemos extrair o resultado. Se, por outro lado, o Either é uma esquerda, sabemos que algo deu errado e podemos extrair a Exception e o valor original, para que possamos prosseguir conforme desejarmos. Usando o Either digite em vez de envolver a Exception marcada em um RuntimeException, evitamos o Stream de terminar no meio do caminho.

Try

Pessoas que podem ter trabalhado com o Scala, por exemplo, podem usar o Try em vez do Either para tratamento de exceção. O tipo Try é algo que é muito semelhante ao outro tipo. Tem, novamente, dois casos: “sucesso” ou “falha”. A falha só pode conter o tipo Exception, enquanto o sucesso pode conter qualquer tipo que você quiser. Então, o Try nada mais é do que uma implementação específica do Either onde o tipo da esquerda (a falha) é corrigido para o tipo Exception.

public class Try<Exception, R> {

    private final Exception failure;
    private final R succes;

    public Try(Exception failure, R succes) {
        this.failure = failure;
        this.succes = succes;
    }
}

Algumas pessoas estão convencidas de que é mais fácil de usar, mas acho que, como só podemos manter a Exception em si na parte da falha, temos o mesmo problema explicado na primeira parte da seção Either. Eu pessoalmente gosto da flexibilidade do tipo Either. De qualquer forma, em ambos os casos, se você usar o Try ou o Either, você resolve o problema inicial do tratamento de exceções e não permite que seu fluxo seja finalizado por causa de uma RuntimeException.

Bibliotecas

Tanto o Either como o Try são muito fáceis de implementar. Por outro lado, você também pode dar uma olhada nas bibliotecas funcionais que estão disponíveis. Por exemplo, o VAVR (anteriormente conhecido como Javaslang) possui implementações para ambos os tipos e funções auxiliares disponíveis. Aconselho-o a dar uma olhada porque tem muito mais do que apenas esses dois tipos. No entanto, você se pergunta se deseja essa biblioteca grande como uma dependência apenas do tratamento de exceções quando é possível implementá-la com apenas algumas linhas de código.

Conclusão

Quando você quer usar um método que lança uma checkedException, você precisa fazer algo extra se quiser chamá-lo em um lambda. Envolvendo-o em uma RuntimeException pode ser uma solução que funcione. Se você preferir usar este método, peço-lhe para criar uma ferramenta de invólucro simples e reutilizá-lo, para que você não se incomode com o try/catch toda vez.

Se você quiser ter mais controle, você pode usar os tipos Either ou Try para envolver o resultado da função, para que você possa manipulá-lo como um dado. O fluxo não terminará quando uma RuntimeException é lançada e você tem a liberdade de manipular os dados dentro de seu fluxo como quiser.