propagate - try catch stream java




Java 8: fluxos de lambda, filtrar por método com exceção (9)

Eu tenho um problema ao experimentar as expressões Lambda do Java 8. Geralmente funciona bem, mas agora eu tenho métodos que lançam o IOException . É melhor se você olhar o seguinte código:

class Bank{
    ....
    public Set<String> getActiveAccountNumbers() throws IOException {
        Stream<Account> s =  accounts.values().stream();
        s = s.filter(a -> a.isActive());
        Stream<String> ss = s.map(a -> a.getNumber());
        return ss.collect(Collectors.toSet());
    }
    ....
}

interface Account{
    ....
    boolean isActive() throws IOException;
    String getNumber() throws IOException;
    ....
}

O problema é que ele não compila, porque eu tenho que pegar as possíveis exceções dos métodos isActive e getNumber. Mas mesmo que eu use explicitamente um bloco try-catch-like abaixo, ele ainda não compila porque eu não pego o Exception. Então, há um bug no JDK ou não sei como capturar essas exceções.

class Bank{
    ....
    //Doesn't compile either
    public Set<String> getActiveAccountNumbers() throws IOException {
        try{
            Stream<Account> s =  accounts.values().stream();
            s = s.filter(a -> a.isActive());
            Stream<String> ss = s.map(a -> a.getNumber());
            return ss.collect(Collectors.toSet());
        }catch(IOException ex){
        }
    }
    ....
}

Como posso fazer isso funcionar? Alguém pode me sugerir a solução certa?


As interfaces funcionais em Java não declaram qualquer exceção marcada ou não verificada. Precisamos mudar a assinatura dos métodos de:

boolean isActive() throws IOException; 
String getNumber() throwsIOException;

Para:

boolean isActive();
String getNumber();

Ou manipulá-lo com o bloco try-catch:

public Set<String> getActiveAccountNumbers() {
  Stream<Account> s =  accounts.values().stream();
  s = s.filter(a -> 
    try{
      a.isActive();
    }catch(IOException e){
      throw new RuntimeException(e);
    }
  );
  Stream<String> ss = s.map(a -> 
    try{
      a.getNumber();
    }catch(IOException e){
      throw new RuntimeException(e);
    }
  );
  return ss.collect(Collectors.toSet());
}

Outra opção é escrever um wrapper customizado ou usar uma biblioteca como ThrowingFunction. Com a biblioteca, só precisamos adicionar a dependência ao nosso pom.xml:

<dependency>
    <groupId>pl.touk</groupId>
    <artifactId>throwing-function</artifactId>
    <version>1.3</version>
</dependency>

E use as classes específicas como ThrowingFunction, ThrowingConsumer, ThrowingPredicate, ThrowingRunnable, ThrowingSupplier.

No final, o código é assim:

public Set<String> getActiveAccountNumbers() {
  return accounts.values().stream()
    .filter(ThrowingPredicate.unchecked(Account::isActive))
    .map(ThrowingFunction.unchecked(Account::getNumber))
    .collect(Collectors.toSet());
}

Essa classe auxiliar UtilException permite usar qualquer exceção verificada em fluxos Java, da seguinte forma:

Stream.of("java.lang.Object", "java.lang.Integer", "java.lang.String")
      .map(rethrowFunction(Class::forName))
      .collect(Collectors.toList());

Nota Class::forName lança ClassNotFoundException , que está marcada . O fluxo em si também lança ClassNotFoundException e NÃO alguma exceção desmarcada.

public final class UtilException {

@FunctionalInterface
public interface Consumer_WithExceptions<T, E extends Exception> {
    void accept(T t) throws E;
    }

@FunctionalInterface
public interface BiConsumer_WithExceptions<T, U, E extends Exception> {
    void accept(T t, U u) throws E;
    }

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

@FunctionalInterface
public interface Supplier_WithExceptions<T, E extends Exception> {
    T get() throws E;
    }

@FunctionalInterface
public interface Runnable_WithExceptions<E extends Exception> {
    void run() throws E;
    }

/** .forEach(rethrowConsumer(name -> System.out.println(Class.forName(name)))); or .forEach(rethrowConsumer(ClassNameUtil::println)); */
public static <T, E extends Exception> Consumer<T> rethrowConsumer(Consumer_WithExceptions<T, E> consumer) throws E {
    return t -> {
        try { consumer.accept(t); }
        catch (Exception exception) { throwAsUnchecked(exception); }
        };
    }

public static <T, U, E extends Exception> BiConsumer<T, U> rethrowBiConsumer(BiConsumer_WithExceptions<T, U, E> biConsumer) throws E {
    return (t, u) -> {
        try { biConsumer.accept(t, u); }
        catch (Exception exception) { throwAsUnchecked(exception); }
        };
    }

/** .map(rethrowFunction(name -> Class.forName(name))) or .map(rethrowFunction(Class::forName)) */
public static <T, R, E extends Exception> Function<T, R> rethrowFunction(Function_WithExceptions<T, R, E> function) throws E {
    return t -> {
        try { return function.apply(t); }
        catch (Exception exception) { throwAsUnchecked(exception); return null; }
        };
    }

/** rethrowSupplier(() -> new StringJoiner(new String(new byte[]{77, 97, 114, 107}, "UTF-8"))), */
public static <T, E extends Exception> Supplier<T> rethrowSupplier(Supplier_WithExceptions<T, E> function) throws E {
    return () -> {
        try { return function.get(); }
        catch (Exception exception) { throwAsUnchecked(exception); return null; }
        };
    }

/** uncheck(() -> Class.forName("xxx")); */
public static void uncheck(Runnable_WithExceptions t)
    {
    try { t.run(); }
    catch (Exception exception) { throwAsUnchecked(exception); }
    }

/** uncheck(() -> Class.forName("xxx")); */
public static <R, E extends Exception> R uncheck(Supplier_WithExceptions<R, E> supplier)
    {
    try { return supplier.get(); }
    catch (Exception exception) { throwAsUnchecked(exception); return null; }
    }

/** uncheck(Class::forName, "xxx"); */
public static <T, R, E extends Exception> R uncheck(Function_WithExceptions<T, R, E> function, T t) {
    try { return function.apply(t); }
    catch (Exception exception) { throwAsUnchecked(exception); return null; }
    }

@SuppressWarnings ("unchecked")
private static <E extends Throwable> void throwAsUnchecked(Exception exception) throws E { throw (E)exception; }

}

Muitos outros exemplos sobre como usá-lo (depois de importar estaticamente o UtilException ):

@Test
public void test_Consumer_with_checked_exceptions() throws IllegalAccessException {
    Stream.of("java.lang.Object", "java.lang.Integer", "java.lang.String")
          .forEach(rethrowConsumer(className -> System.out.println(Class.forName(className))));

    Stream.of("java.lang.Object", "java.lang.Integer", "java.lang.String")
          .forEach(rethrowConsumer(System.out::println));
    }

@Test
public void test_Function_with_checked_exceptions() throws ClassNotFoundException {
    List<Class> classes1
          = Stream.of("Object", "Integer", "String")
                  .map(rethrowFunction(className -> Class.forName("java.lang." + className)))
                  .collect(Collectors.toList());

    List<Class> classes2
          = Stream.of("java.lang.Object", "java.lang.Integer", "java.lang.String")
                  .map(rethrowFunction(Class::forName))
                  .collect(Collectors.toList());
    }

@Test
public void test_Supplier_with_checked_exceptions() throws ClassNotFoundException {
    Collector.of(
          rethrowSupplier(() -> new StringJoiner(new String(new byte[]{77, 97, 114, 107}, "UTF-8"))),
          StringJoiner::add, StringJoiner::merge, StringJoiner::toString);
    }

@Test    
public void test_uncheck_exception_thrown_by_method() {
    Class clazz1 = uncheck(() -> Class.forName("java.lang.String"));

    Class clazz2 = uncheck(Class::forName, "java.lang.String");
    }

@Test (expected = ClassNotFoundException.class)
public void test_if_correct_exception_is_still_thrown_by_method() {
    Class clazz3 = uncheck(Class::forName, "INVALID");
    }

Mas não o use antes de entender as seguintes vantagens, desvantagens e limitações :

• Se o código de chamada for manipular a exceção verificada, você DEVE adicioná-lo à cláusula throws do método que contém o fluxo. O compilador não irá forçá-lo a adicioná-lo mais, então é mais fácil esquecê-lo.

• Se o código de chamada já manipular a exceção verificada, o compilador irá lembrá-lo de adicionar a cláusula throws à declaração do método que contém o fluxo (se você não disser: A exceção nunca é lançada no corpo da instrução try correspondente ).

• Em qualquer caso, você não poderá cercar o fluxo para capturar a exceção verificada dentro do método que contém o fluxo (se você tentar, o compilador dirá: A exceção nunca é lançada no corpo da instrução try correspondente).

• Se você está chamando um método que literalmente nunca pode lançar a exceção declarada, então você não deve incluir a cláusula throws. Por exemplo: new String (byteArr, "UTF-8") lança UnsupportedEncodingException, mas UTF-8 é garantido pela especificação Java para sempre estar presente. Aqui, a declaração de arremesso é um incômodo e qualquer solução para silenciá-lo com o mínimo de clichê é bem-vinda.

• Se você odeia as exceções verificadas e acha que elas nunca devem ser adicionadas à linguagem Java (um número crescente de pessoas pensa assim, e eu NÃO sou uma delas), então não adicione a exceção verificada ao throws cláusula do método que contém o fluxo. A exceção verificada, então, se comportará exatamente como uma exceção não verificada.

• Se você está implementando uma interface estrita onde você não tem a opção de adicionar uma declaração throws, e ainda lançar uma exceção é inteiramente apropriado, então quebrar uma exceção apenas para obter o privilégio de lançá-la resulta em um stacktrace com exceções espúrias que contribuem sem informações sobre o que realmente deu errado. Um bom exemplo é o Runnable.run (), que não lança nenhuma exceção verificada. Nesse caso, você pode decidir não adicionar a exceção verificada à cláusula throws do método que contém o fluxo.

• Em qualquer caso, se você decidir NÃO adicionar (ou esquecer de adicionar) a exceção marcada à cláusula throws do método que contém o fluxo, esteja ciente dessas duas conseqüências de lançar exceções CHECKED:

1) O código de chamada não será capaz de pegá-lo pelo nome (se você tentar, o compilador dirá: A exceção nunca é lançada no corpo da instrução try correspondente). Ele irá borbulhar e provavelmente será capturado no loop principal do programa por alguns "catch Exception" ou "catch Throwable", que podem ser o que você quiser de qualquer maneira.

2) Viola o princípio da menor surpresa: não será mais suficiente capturar o RuntimeException para garantir a captura de todas as possíveis exceções. Por esse motivo, acredito que isso não deva ser feito no código da estrutura, mas apenas no código comercial que você controla completamente.

Em conclusão: Acredito que as limitações aqui não são sérias, e a classe UtilException pode ser usada sem medo. No entanto, depende de você!


Isso não responde diretamente à pergunta (há muitas outras respostas), mas tenta evitar o problema em primeiro lugar:

Na minha experiência, a necessidade de lidar com exceções em um Stream (ou outra expressão lambda) geralmente vem do fato de que as exceções são declaradas como sendo lançadas a partir de métodos nos quais elas não devem ser lançadas. Isso geralmente vem da mistura de lógica de negócios com entrada e saída. A interface da sua Account é um exemplo perfeito:

interface Account {
    boolean isActive() throws IOException;
    String getNumber() throws IOException;
}

Em vez de lançar uma IOException em cada getter, considere este design:

interface AccountReader {
    Account readAccount(…) throws IOException;
}

interface Account {
    boolean isActive();
    String getNumber();
}

O método AccountReader.readAccount(…) poderia ler uma conta de um banco de dados ou um arquivo ou qualquer outra coisa e lançar uma exceção se não tiver êxito. Ele constrói um objeto Account que já contém todos os valores, prontos para serem usados. Como os valores já foram carregados por readAccount(…) , os getters não lançariam uma exceção. Assim você pode usá-los livremente em lambdas sem a necessidade de embrulhar, mascarar ou esconder as exceções.

É claro que nem sempre é possível fazer da maneira que descrevi, mas muitas vezes é e leva a um código mais limpo (IMHO):

  • Melhor separação de preocupações e seguindo o princípio da responsabilidade única
  • Menos clichê: você não tem que confundir seu código com throws IOException para não usar, mas para satisfazer o compilador
  • Tratamento de erros: você lida com os erros onde eles acontecem - ao ler de um arquivo ou banco de dados - em vez de em algum lugar no meio de sua lógica de negócios apenas porque você deseja obter um valor de campos
  • Você pode ser capaz de tornar a Account immutable e lucrar com as vantagens dela (por exemplo, segurança de thread)
  • Você não precisa de "truques sujos" ou soluções alternativas para usar a Account em lambdas (por exemplo, em um Stream )

Mantendo este problema em mente, desenvolvi uma pequena biblioteca para lidar com exceções verificadas e lambdas. Adaptadores personalizados permitem integrar com tipos funcionais existentes:

stream().map(unchecked(URI::new)) //with a static import

https://github.com/TouK/ThrowingFunction/


Pode ser resolvido pelo código abaixo simples com Stream e Try in AbacusUtil :

Stream.of(accounts).filter(a -> Try.call(a::isActive)).map(a -> Try.call(a::getNumber)).toSet();

Divulgação: Eu sou o desenvolvedor do AbacusUtil .


Se você não se importa em usar bibliotecas de terceiros, a biblioteca cyclops-react da AOL, disclosure :: Eu sou um colaborador, tem uma classe ExceptionSoftener que pode ajudar aqui.

 s.filter(softenPredicate(a->a.isActive()));

Use o método #propagate (). Amostra de implementação não-goiaba do blog Java 8 de Sam Beran :

public class Throwables {
    public interface ExceptionWrapper<E> {
        E wrap(Exception e);
    }

    public static <T> T propagate(Callable<T> callable) throws RuntimeException {
        return propagate(callable, RuntimeException::new);
    }

    public static <T, E extends Throwable> T propagate(Callable<T> callable, ExceptionWrapper<E> wrapper) throws E {
        try {
            return callable.call();
        } catch (RuntimeException e) {
            throw e;
        } catch (Exception e) {
            throw wrapper.wrap(e);
        }
    }
}

Você deve pegar a exceção antes de escapar do lambda:

s = s.filter(a -> { try { return a.isActive(); } 
                    catch (IOException e) { throw new UncheckedIOException(e); }}});

Considere o fato de que o lambda não é avaliado no local em que você o escreve, mas em algum lugar completamente diferente, dentro de uma classe JDK. Então, esse seria o ponto em que a exceção verificada seria lançada e, nesse local, não seria declarada.

Você pode lidar com isso usando um wrapper do seu lambda que traduz exceções verificadas para desmarcadas:

public static <T> T uncheckCall(Callable<T> callable) {
  try { return callable.call(); }
  catch (RuntimeException e) { throw e; }
  catch (Exception e) { throw new RuntimeException(e); }
}

Seu exemplo seria escrito como

return s.filter(a -> uncheckCall(a::isActive))
        .map(Account::getNumber)
        .collect(toSet());

Nos meus projetos lidei com esse problema sem embrulhar; em vez disso, uso um método que efetivamente desabilita a verificação de exceções do compilador. Escusado será dizer que isto deve ser tratado com cuidado e todos no projeto devem estar cientes de que uma exceção verificada pode aparecer onde não é declarada. Este é o código de encanamento:

public static <T> T uncheckCall(Callable<T> callable) {
  try { return callable.call(); }
  catch (Exception e) { return sneakyThrow(e); }
}
public static void uncheckRun(RunnableExc r) {
  try { r.run(); } catch (Exception e) { sneakyThrow(e); }
}
public interface RunnableExc { void run() throws Exception; }


@SuppressWarnings("unchecked")
private static <T extends Throwable> void sneakyThrow(Throwable t) throws T {
  throw (T) t;
}

e você pode esperar obter uma IOException na sua cara, mesmo que a collect não a declare. Na maioria, mas não em todos os casos da vida real, você gostaria apenas de relançar a exceção, de qualquer maneira, e lidar com isso como uma falha genérica. Em todos esses casos, nada se perde em clareza ou correção. Apenas tome cuidado com esses outros casos, onde você realmente quer reagir à exceção no local. O desenvolvedor não será avisado pelo compilador que existe uma IOException para capturar lá e o compilador irá de fato reclamar se você tentar pegá-lo porque nós o enganamos acreditando que nenhuma exceção pode ser lançada.


Você também pode propagar sua dor estática com lambdas, então a coisa toda parece legível:

s.filter(a -> propagate(a::isActive))

propagate aqui recebe java.util.concurrent.Callable como um parâmetro e converte qualquer exceção capturada durante a chamada em RuntimeException . Existe um método de conversão semelhante Throwables#propagate(Throwable) em Goiaba.

Esse método parece ser essencial para o encadeamento do método lambda, então espero que um dia ele seja adicionado a uma das bibliotecas populares ou esse comportamento de propagação seja por padrão.

public class PropagateExceptionsSample {
    // a simplified version of Throwables#propagate
    public static RuntimeException runtime(Throwable e) {
        if (e instanceof RuntimeException) {
            return (RuntimeException)e;
        }

        return new RuntimeException(e);
    }

    // this is a new one, n/a in public libs
    // Callable just suits as a functional interface in JDK throwing Exception 
    public static <V> V propagate(Callable<V> callable){
        try {
            return callable.call();
        } catch (Exception e) {
            throw runtime(e);
        }
    }

    public static void main(String[] args) {
        class Account{
            String name;    
            Account(String name) { this.name = name;}

            public boolean isActive() throws IOException {
                return name.startsWith("a");
            }
        }


        List<Account> accounts = new ArrayList<>(Arrays.asList(new Account("andrey"), new Account("angela"), new Account("pamela")));

        Stream<Account> s = accounts.stream();

        s
          .filter(a -> propagate(a::isActive))
          .map(a -> a.name)
          .forEach(System.out::println);
    }
}




java-8