it-swarm.com.ru

Java 8: лямбда-потоки, фильтрация по методам с исключением

У меня возникла проблема при проверке лямбда-выражений Java 8 . Обычно это работает нормально, но теперь у меня есть методы, которые выбрасывают IOException. Лучше всего, если вы посмотрите на следующий код:

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;
    ....
}

Проблема в том, что он не компилируется, потому что мне нужно перехватить возможные исключения из isActive- и getNumber-Methods. Но даже если я явно использую блок try-catch-Block, как показано ниже, он все равно не скомпилируется, потому что я не улавливаю исключение. Так что либо в JDK есть ошибка, либо я не знаю, как перехватить эти исключения.

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){
        }
    }
    ....
}

Как я могу заставить это работать? Может кто-нибудь подсказать мне правильное решение?

149
Martin Weber

Вы должны перехватить исключение до того, как оно выйдет из лямбды:

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

Примите во внимание тот факт, что лямбда не оценивается в том месте, где вы ее пишете, а в каком-то совершенно не связанном месте, в классе JDK. Так что это будет точка, где будет выброшено это проверенное исключение, и в этом месте оно не объявлено.

Вы можете справиться с этим, используя оболочку вашей лямбды, которая переводит проверенные исключения в непроверенные:

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

Ваш пример будет написан как

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

В своих проектах я занимаюсь этим вопросом без обертывания; вместо этого я использую метод, который эффективно предотвращает проверку исключений компилятором. Нет необходимости говорить, что с этим нужно обращаться осторожно, и все участники проекта должны знать, что проверенное исключение может появиться там, где оно не объявлено. Это код слесарного дела:

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;
}

и вы можете ожидать, что вы получите IOException , даже если collect не объявит его. В большинстве, но не во всех реальных случаях вы все равно захотите просто сбросить исключение и обработать его как общий сбой. Во всех этих случаях ничего не теряется в ясности или правильности. Просто остерегайтесь тех других случаев, когда вы действительно захотите отреагировать на исключение на месте. Разработчик не будет уведомлен компилятором о том, что существует переменная IOException, и компилятор на самом деле будет жаловаться, если вы попытаетесь его перехватить, потому что мы обманули его, полагая, что такое исключение не может быть сгенерировано.

191
Marko Topolnik

Вы также можете распространять свою статическую боль с помощью лямбд, так что все это выглядит читабельно:

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

Здесь propagate получает Java.util.concurrent.Callable в качестве параметра и преобразует любое исключение, перехваченное во время вызова, в RuntimeException. Существует аналогичный метод преобразования Throwables # размножаться (Throwable) в Гуаве.

Этот метод, по-видимому, необходим для цепочки лямбда-методов, поэтому я надеюсь, что однажды он будет добавлен к одной из популярных библиотек или это распространяющееся поведение будет по умолчанию.

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);
    }
}
26
Andrey Chaschev

Этот вспомогательный класс UtilException позволяет вам использовать любые проверенные исключения в потоках Java, например:

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

Примечание Class::forName выдает ClassNotFoundException, который является checked. Сам поток также генерирует ClassNotFoundException, а НЕ какое-то исключение без проверки.

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; }

}

Много других примеров того, как его использовать (после статического импорта 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");
    }

Но не используйте его до понимания следующих преимуществ, недостатков и ограничений:

• Если вызывающий код должен обрабатывать проверенное исключение, вы ДОЛЖНЫ добавить его в предложение throws метода, который содержит поток . Компилятор больше не будет заставлять вас добавлять его, поэтому его легче забыть.

• Если вызывающий код уже обрабатывает проверенное исключение, компилятор напомнит вам добавить предложение throws к объявлению метода который содержит поток (если вы этого не сделаете, он скажет: исключение никогда не выдается в теле соответствующего оператора try).

• В любом случае вы не сможете окружить сам поток, чтобы поймать проверенное исключение ВНУТРИ метода, который содержит поток (если вы попробуете, компилятор скажет: исключение никогда не выдается в теле соответствующего оператора try).

• Если вы вызываете метод, который буквально никогда не может сгенерировать исключение, которое он объявляет, вы не должны включать предложение throws . Например: new String (byteArr, "UTF-8") выбрасывает UnsupportedEncodingException, но UTF-8 гарантируется, что спецификация Java всегда присутствует . Здесь, декларация бросков - неприятность, и любое решение заставить ее замолчать с минимальными шаблонами приветствуется.

• Если вы ненавидите проверенные исключения и чувствуете, что их никогда не следует добавлять в язык Java для начала (все больше людей думают так, А я НЕ один из них), то просто не добавляйте проверенные исключение из условия throws метода, который содержит поток. Проверено исключение будет вести себя так же, как исключение UNchecked.

• Если вы реализуете строгий интерфейс, в котором у вас нет возможности добавить объявление throws, но выдается исключение вполне уместно, затем обертывание исключения только для того, чтобы получить привилегию его выброса, приводит к трассировке стека с ложными исключениями, которые не вносить никакой информации о том, что на самом деле пошло не так. Хорошим примером является Runnable.run (), который не выдает никаких проверенных исключений . В этом случае вы можете решить не добавлять проверенное исключение в предложение throws метода, содержащего поток.

• В любом случае, если вы решите НЕ добавлять (или забыть добавить) отмеченное исключение в предложение throws метода, содержащего поток, Помните об этих 2 последствиях исключения CHECKED:

1) Вызывающий код не сможет поймать его по имени (если вы попытаетесь, компилятор скажет: исключение никогда не будет выдано в теле соответствующего оператора try ). Он будет пузыриться и, вероятно, будет перехвачен в цикле основной программы каким-то «catch Exception» или «catch Throwable», который может быть тем, что вы хочу в любом случае.

2) Это нарушает принцип наименьшего удивления: вам больше не будет достаточно перехватить RuntimeException, чтобы иметь возможность гарантировать перехват всех возможные исключения. По этой причине я считаю, что это следует делать не в программном коде, а только в бизнес-коде, который вы полностью контролируете.

В заключение: я считаю, что ограничения здесь несерьезны, и класс UtilException можно использовать без страха. Тем не менее, это зависит от вас!

17
MarcG

Вы можете потенциально свернуть свой собственный вариант Stream, обернув лямбду, чтобы создать исключение без проверки, а затем распаковать это исключение без проверки в операциях терминала:

@FunctionalInterface
public interface ThrowingPredicate<T, X extends Throwable> {
    public boolean test(T t) throws X;
}

@FunctionalInterface
public interface ThrowingFunction<T, R, X extends Throwable> {
    public R apply(T t) throws X;
}

@FunctionalInterface
public interface ThrowingSupplier<R, X extends Throwable> {
    public R get() throws X;
}

public interface ThrowingStream<T, X extends Throwable> {
    public ThrowingStream<T, X> filter(
            ThrowingPredicate<? super T, ? extends X> predicate);

    public <R> ThrowingStream<T, R> map(
            ThrowingFunction<? super T, ? extends R, ? extends X> mapper);

    public <A, R> R collect(Collector<? super T, A, R> collector) throws X;

    // etc
}

class StreamAdapter<T, X extends Throwable> implements ThrowingStream<T, X> {
    private static class AdapterException extends RuntimeException {
        public AdapterException(Throwable cause) {
            super(cause);
        }
    }

    private final Stream<T> delegate;
    private final Class<X> x;

    StreamAdapter(Stream<T> delegate, Class<X> x) {
        this.delegate = delegate;
        this.x = x;
    }

    private <R> R maskException(ThrowingSupplier<R, X> method) {
        try {
            return method.get();
        } catch (Throwable t) {
            if (x.isInstance(t)) {
                throw new AdapterException(t);
            } else {
                throw t;
            }
        }
    }

    @Override
    public ThrowingStream<T, X> filter(ThrowingPredicate<T, X> predicate) {
        return new StreamAdapter<>(
                delegate.filter(t -> maskException(() -> predicate.test(t))), x);
    }

    @Override
    public <R> ThrowingStream<R, X> map(ThrowingFunction<T, R, X> mapper) {
        return new StreamAdapter<>(
                delegate.map(t -> maskException(() -> mapper.apply(t))), x);
    }

    private <R> R unmaskException(Supplier<R> method) throws X {
        try {
            return method.get();
        } catch (AdapterException e) {
            throw x.cast(e.getCause());
        }
    }

    @Override
    public <A, R> R collect(Collector<T, A, R> collector) throws X {
        return unmaskException(() -> delegate.collect(collector));
    }
}

Тогда вы можете использовать это точно так же, как Stream:

Stream<Account> s = accounts.values().stream();
ThrowingStream<Account, IOException> ts = new StreamAdapter<>(s, IOException.class);
return ts.filter(Account::isActive).map(Account::getNumber).collect(toSet());

Для этого решения потребовалось бы совсем немного стандартного, поэтому я предлагаю вам взглянуть на библиотеку, которую я уже создал которая в точности соответствует тому, что я описал здесь для всего класса Stream (и больше!).

8
Jeffrey

Используйте метод #propagate (). Пример реализации не-Гуавы из Блог Java 8 Сэма Берана :

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);
        }
    }
}
5
n0mer

Чтобы правильно добавить код обработки IOException (в RuntimeException), ваш метод будет выглядеть следующим образом:

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

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

Stream<String> ss = s.map(a -> { try { return a.getNumber() }
  catch (IOException e) { throw new RuntimeException(e); }});

return ss.collect(Collectors.toSet());

Теперь проблема в том, что IOException нужно будет захватить как RuntimeException и преобразовать обратно в IOException - и это добавит еще больше кода к вышеуказанному методу.

Зачем использовать Stream, когда это можно сделать именно так - и метод выдает IOException, поэтому для этого тоже не требуется никакого дополнительного кода:

Set<String> set = new HashSet<>();
for(Account a: accounts.values()){
  if(a.isActive()){
     set.add(a.getNumber());
  } 
}
return set;
3
The Coordinator

Это может быть решено с помощью простого кода ниже с помощью Stream и Try in AbacusUtil :

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

Раскрытие информации: я разработчик AbacusUtil.

3
user_3380739

Это не дает прямого ответа на вопрос (есть много других ответов), но в первую очередь пытается избежать проблемы:

По моему опыту, необходимость обрабатывать исключения в Stream (или другом лямбда-выражении) часто возникает из-за того, что исключения объявляются как методы, которые не должны быть выброшены. Часто это происходит из-за смешивания бизнес-логики с входом и выходом. Ваш интерфейс Account является прекрасным примером:

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

Вместо того, чтобы бросать IOException на каждый получатель, рассмотрите этот дизайн:

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

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

Метод AccountReader.readAccount(…) может считывать учетную запись из базы данных, файла или чего-либо еще и выдавать исключение, если это не удается. Он создает объект Account, который уже содержит все значения, готовые к использованию. Поскольку значения уже были загружены функцией readAccount(…), получатели не будут генерировать исключение. Таким образом, вы можете свободно использовать их в лямбдах без необходимости оборачивать, маскировать или скрывать исключения.

Конечно, это не всегда возможно сделать так, как я описал, но часто это так и приводит к более чистому коду (ИМХО):

  • Лучше разделение интересов и следование принцип единой ответственности
  • Меньше шаблонного: вам не нужно загромождать свой код с помощью throws IOException, чтобы не использовать его, но чтобы удовлетворить компилятор
  • Обработка ошибок: вы обрабатываете ошибки там, где они возникают - при чтении из файла или базы данных - вместо того, чтобы где-то в середине вашей бизнес-логики, только потому, что вы хотите получить значение поля
  • Вы можете сделать Accountнеизменяемым и использовать его преимущества (например, безопасность потоков)
  • Вам не нужны "грязные уловки" или обходные пути для использования Account в лямбдах (например, в Stream)
3
siegi

Расширяя решение @marcg, вы обычно можете бросить и поймать проверено исключение в Streams; то есть, компилятор попросит вас перехватить/перебросить как вы были вне потоков !!

@FunctionalInterface
public interface Predicate_WithExceptions<T, E extends Exception> {
    boolean test(T t) throws E;
}

/**
 * .filter(rethrowPredicate(t -> t.isActive()))
 */
public static <T, E extends Exception> Predicate<T> rethrowPredicate(Predicate_WithExceptions<T, E> predicate) throws E {
    return t -> {
        try {
            return predicate.test(t);
        } catch (Exception exception) {
            return throwActualException(exception);
        }
    };
}

@SuppressWarnings("unchecked")
private static <T, E extends Exception> T throwActualException(Exception exception) throws E {
    throw (E) exception;
}

Тогда ваш пример будет записан следующим образом (добавление тестов, чтобы показать это более четко):

@Test
public void testPredicate() throws MyTestException {
    List<String> nonEmptyStrings = Stream.of("ciao", "")
            .filter(rethrowPredicate(s -> notEmpty(s)))
            .collect(toList());
    assertEquals(1, nonEmptyStrings.size());
    assertEquals("ciao", nonEmptyStrings.get(0));
}

private class MyTestException extends Exception { }

private boolean notEmpty(String value) throws MyTestException {
    if(value==null) {
        throw new MyTestException();
    }
    return !value.isEmpty();
}

@Test
public void testPredicateRaisingException() throws MyTestException {
    try {
        Stream.of("ciao", null)
                .filter(rethrowPredicate(s -> notEmpty(s)))
                .collect(toList());
        fail();
    } catch (MyTestException e) {
        //OK
    }
}
2
PaoloC

Ваш пример может быть записан как:

import utils.stream.Unthrow;

class Bank{
   ....
   public Set<String> getActiveAccountNumbers() {
       return accounts.values().stream()
           .filter(a -> Unthrow.wrap(() -> a.isActive()))
           .map(a -> Unthrow.wrap(() -> a.getNumber()))
           .collect(Collectors.toSet());
   }
   ....
}

Класс Unthrow можно взять здесь https://github.com/SeregaLBN/StreamUnthrower

1
SeregaLBN

Помня об этой проблеме, я разработал небольшую библиотеку для работы с проверенными исключениями и лямбдами. Пользовательские адаптеры позволяют интегрироваться с существующими функциональными типами:

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

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

1
Grzegorz Piwowarek

Функциональные интерфейсы в Java не объявляют ни одно проверенное или непроверенное исключение . Нам нужно изменить сигнатуру методов с:

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

Для того, чтобы:

boolean isActive();
String getNumber();

Или обработайте его блоком 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());
}

Другой вариант - написать пользовательскую оболочку или использовать библиотеку, такую ​​как ThrowingFunction . С библиотекой нам нужно только добавить зависимость в наш pom.xml:

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

И использовать определенные классы, такие как ThrowingFunction, ThrowingConsumer, ThrowingPredicate, ThrowingRunnable, ThrowingSupplier.

В конце код выглядит так:

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

Если вы не возражаете против использования сторонних библиотек, у AOL cyclops-реагировать lib, disclosure :: Я участвую, есть класс ExceptionSoftener , который может помочь здесь.

 s.filter(softenPredicate(a->a.isActive()));
0
John McClean