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