Совокупные исключения времени выполнения в потоках Java 8

Скажем, у меня есть метод, который генерирует исключение во время выполнения. Я использую Stream для вызова этого метода для элементов в списке.

class ABC {

    public void doStuff(MyObject myObj) {
        if (...) {
            throw new IllegalStateException("Fire! Fear! Foes! Awake!");
        }
        // do stuff...
    }

    public void doStuffOnList(List<MyObject> myObjs) {
        try {
            myObjs.stream().forEach(ABC:doStuff);
        } catch(AggregateRuntimeException??? are) {
            ...
        }             
    }
}

Теперь я хочу, чтобы все элементы в списке были обработаны, и любые исключения во время выполнения для отдельных элементов, которые должны быть собраны в "суммарное" время выполнения, которое будет выбрано в конце.

В моем реальном коде я делаю сторонние API-вызовы, которые могут вызывать исключения во время выполнения. Я хочу удостовериться, что все элементы обработаны, и все ошибки сообщаются в конце.

Я могу придумать несколько способов взломать это, например, функцию map(), которая ловит и возвращает исключение (..shudder..). Но есть ли у нас родной способ сделать это? Если нет, есть ли другой способ реализовать его чисто?

Ответ 1

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

myObjs.stream()
    .flatMap(o -> {
        try {
            ABC.doStuff(o);
            return null;
        } catch (RuntimeException ex) {
            return Stream.of(ex);
        }
    })
    // now a stream of thrown exceptions.
    // can collect them to list or reduce into one exception
    .reduce((ex1, ex2) -> {
        ex1.addSuppressed(ex2);
        return ex1;
    }).ifPresent(ex -> {
        throw ex;
    });

Однако, если ваши требования более сложны и вы предпочитаете придерживаться стандартной библиотеки, CompletableFuture может служить для представления "успеха или неудачи" (хотя и с некоторыми бородавками):

public static void doStuffOnList(List<MyObject> myObjs) {
    myObjs.stream()
            .flatMap(o -> completedFuture(o)
                    .thenAccept(ABC::doStuff)
                    .handle((x, ex) -> ex != null ? Stream.of(ex) : null)
                    .join()
            ).reduce((ex1, ex2) -> {
                ex1.addSuppressed(ex2);
                return ex1;
            }).ifPresent(ex -> {
                throw new RuntimeException(ex);
            });
}

Ответ 2

Есть уже некоторые реализации Try monad для Java. Например, я нашел лучшую-java8-monads библиотеку. Используя его, вы можете писать в следующем стиле.

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

public String doStuff(String s) {
    if(s.startsWith("a")) {
        throw new IllegalArgumentException("Incorrect string: "+s);
    }
    return s.trim();
}

Пусть есть некоторый ввод:

List<String> input = Arrays.asList("aaa", "b", "abc  ", "  qqq  ");

Теперь мы можем сопоставить их с успешными попытками и перейти к вашему методу, а затем собрать успешно обработанные данные и сбои отдельно:

Map<Boolean, List<Try<String>>> result = input.stream()
        .map(Try::successful).map(t -> t.map(this::doStuff))
        .collect(Collectors.partitioningBy(Try::isSuccess));

После этого вы можете обрабатывать успешные записи:

System.out.println(result.get(true).stream()
    .map(t -> t.orElse(null)).collect(Collectors.joining(",")));

И сделайте что-нибудь со всеми исключениями:

result.get(false).stream().forEach(t -> t.onFailure(System.out::println));

Вывод:

b,qqq
java.lang.IllegalArgumentException: Incorrect string: aaa
java.lang.IllegalArgumentException: Incorrect string: abc  

Мне лично не нравится, как эта библиотека создана, но, вероятно, она вам подойдет.

Здесь gist с полным примером.

Ответ 3

Здесь вариация темы сопоставления к исключениям.

Начните с существующего метода doStuff. Обратите внимание, что это соответствует функциональному интерфейсу Consumer<MyObject>.

public void doStuff(MyObject myObj) {
    if (...) {
        throw new IllegalStateException("Fire! Fear! Foes! Awake!");
    }
    // do stuff...
}

Теперь напишите функцию более высокого порядка, которая обертывает это и превращает это в функцию, которая могла бы или не могла бы возвратить исключение. Мы хотим называть это от flatMap, поэтому способ "может или не быть" выражается путем возврата потока, содержащего исключение или пустой поток. Я использую RuntimeException как тип исключения здесь, но, конечно, это может быть что угодно. (На самом деле было бы полезно использовать этот метод с проверенными исключениями.)

<T> Function<T,Stream<RuntimeException>> ex(Consumer<T> cons) {
    return t -> {
        try {
            cons.accept(t);
            return Stream.empty();
        } catch (RuntimeException re) {
            return Stream.of(re);
        }
    };
}

Теперь перепишите doStuffOnList, чтобы использовать это в потоке:

void doStuffOnList(List<MyObject> myObjs) {
    List<RuntimeException> exs =
        myObjs.stream()
              .flatMap(ex(this::doStuff))
              .collect(Collectors.toList());
    System.out.println("Exceptions: " + exs);
}

Ответ 4

Единственный возможный способ, который я могу себе представить, - сопоставить значения в списке с монадой, которая будет представлять результат выполнения вашей обработки (либо успех со значением или сбой с помощью throwable). Затем сверните поток в единый результат с агрегированным списком значений или одним исключением со списком подавленных из предыдущих шагов.

public Result<?> doStuff(List<?> list) {
     return list.stream().map(this::process).reduce(RESULT_MERGER)
}

public Result<SomeType> process(Object listItem) {
    try {
         Object result = /* Do the processing */ listItem;
         return Result.success(result);
    } catch (Exception e) {
         return Result.failure(e);
    }
}

public static final BinaryOperator<Result<?>> RESULT_MERGER = (left, right) -> left.merge(right)

Реализация результата может отличаться, но я думаю, что вы поняли эту идею.