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

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

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-методов. Но даже если я явно использую блок try-catch, как показано ниже, он все равно не компилируется, потому что я не улавливаю исключение. Так что либо есть ошибка в 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){
        }
    }
    ....
}

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

12 ответов


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

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 бросил тебе в лицо, хотя ... --5--> не объявляет его. В большинство, но не все реальные случаи вы хотели бы просто переосмыслить исключение, в любом случае, и обрабатывать его как общий сбой. Во всех этих случаях ничто не теряется в ясности или правильности. Просто остерегайтесь тех других случаев, когда вы действительно хотите отреагировать на исключение на месте. Разработчик не будет уведомлен компилятором о том, что существует IOException ловить там и компилятор будет на самом деле жалуйтесь, если вы пытаетесь поймать его, потому что мы обманули его, полагая, что такое исключение не может быть брошено.


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

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

propagate здесь получает java.util.concurrent.Callable в качестве параметра и преобразует любое исключение во время вызова RuntimeException. Существует аналогичный метод преобразования Throwables#propagate (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);
    }
}

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

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

Примечание Class::forName закидываем ClassNotFoundException, которая составляет проверил. Сам поток также бросает 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 метода, которое содержит поток. Проверено таким образом, исключение будет вести себя как непроверенное исключение.

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

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

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

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

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


вы можете свернуть свой собственный 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 класс (и многое другое!).


используйте метод #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);
        }
    }
}

Это может быть разрешено ниже простым кодом с поток и попробовать на AbacusUtil:

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

: раскрытие я разработчик AbacusUtil.


чтобы правильно добавить код обработки IOException (to 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;

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

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

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

по моему опыту, необходимо обрабатывать исключения в 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(…), геттеры не будут бросать исключение. Таким образом, вы можете свободно использовать их в лямбдами без нужно обернуть, замаскировать или скрыть исключения.

конечно, это не всегда возможно сделать так, как я описал, но часто это так, и это приводит к более чистому коду вообще (IMHO):

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

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

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

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


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

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


Если вы не против использования сторонних библиотек, aol's Циклоп-реагировать lib, раскрытие информации:: я являюсь участником, имеет ExceptionSoftener класс, который может помочь здесь.

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