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