Совокупные исключения времени выполнения в потоках 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..). Но есть ли родной способ сделать это? Если нет, есть ли другой способ реализовать это чисто?

4 ответа

Решение

В этом простом случае, когда 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);
            });
}

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

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

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  

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

Вот суть с полным примером.

Вот вариация на тему сопоставления с исключениями.

Начните с вашего существующего 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);
}

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

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

Другие вопросы по тегам