Disons que j'ai une méthode qui lève une exception d'exécution. J'utilise un Stream
pour appeler cette méthode sur les éléments d'une liste.
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) {
...
}
}
}
Maintenant, je veux que tous les éléments de la liste soient traités et que toutes les exceptions d'exécution sur des éléments individuels soient collectées dans une exception d'exécution "agrégée" qui sera levée à la fin.
Dans mon vrai code, je fais des appels API tiers qui peuvent lever des exceptions d'exécution. Je veux m'assurer que tous les articles sont traités et toutes les erreurs signalées à la fin.
Je peux penser à quelques façons de pirater cela, comme une fonction map()
qui intercepte et renvoie l'exception (.. shudder ..). Mais existe-t-il une manière native de procéder? Sinon, existe-t-il un autre moyen de le mettre en œuvre proprement?
Dans ce cas simple où la méthode doStuff
est void
et que vous ne vous souciez que des exceptions, vous pouvez garder les choses simples:
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;
});
Cependant, si vos exigences sont plus compliquées et que vous préférez vous en tenir à la bibliothèque standard, CompletableFuture
peut servir à représenter "soit le succès, soit l'échec" (quoique avec certaines verrues):
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);
});
}
Il existe déjà quelques implémentations de Try
monad pour Java. J'ai trouvé better-Java8-monads bibliothèque, par exemple. En l'utilisant, vous pouvez écrire dans le style suivant.
Supposons que vous souhaitiez mapper vos valeurs et suivre toutes les exceptions:
public String doStuff(String s) {
if(s.startsWith("a")) {
throw new IllegalArgumentException("Incorrect string: "+s);
}
return s.trim();
}
Ayons une entrée:
List<String> input = Arrays.asList("aaa", "b", "abc ", " qqq ");
Nous pouvons maintenant les mapper sur des tentatives réussies et passer à votre méthode, puis collecter séparément les données et les échecs traités avec succès:
Map<Boolean, List<Try<String>>> result = input.stream()
.map(Try::successful).map(t -> t.map(this::doStuff))
.collect(Collectors.partitioningBy(Try::isSuccess));
Après cela, vous pouvez traiter les entrées réussies:
System.out.println(result.get(true).stream()
.map(t -> t.orElse(null)).collect(Collectors.joining(",")));
Et faites quelque chose avec toutes les exceptions:
result.get(false).stream().forEach(t -> t.onFailure(System.out::println));
La sortie est:
b,qqq
Java.lang.IllegalArgumentException: Incorrect string: aaa
Java.lang.IllegalArgumentException: Incorrect string: abc
Personnellement, je n'aime pas la façon dont cette bibliothèque est conçue, mais elle vous conviendra probablement.
Voici un Gist avec un exemple complet.
Voici une variation sur le thème du mappage vers les exceptions.
Commencez avec votre méthode doStuff
existante. Notez que cela est conforme à l'interface fonctionnelle Consumer<MyObject>
.
public void doStuff(MyObject myObj) {
if (...) {
throw new IllegalStateException("Fire! Fear! Foes! Awake!");
}
// do stuff...
}
Maintenant, écrivez une fonction d'ordre supérieur qui encapsule cela et le transforme en une fonction qui peut ou non renvoyer une exception. Nous voulons appeler cela à partir de flatMap
, donc la façon dont "pourrait ou non" est exprimée est en renvoyant un flux contenant l'exception ou un flux vide. J'utiliserai RuntimeException
comme type d'exception ici, mais bien sûr, cela pourrait être n'importe quoi. (En fait, il pourrait être utile d'utiliser cette technique avec des exceptions vérifiées.)
<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);
}
};
}
Maintenant, réécrivez doStuffOnList
pour l'utiliser dans un flux:
void doStuffOnList(List<MyObject> myObjs) {
List<RuntimeException> exs =
myObjs.stream()
.flatMap(ex(this::doStuff))
.collect(Collectors.toList());
System.out.println("Exceptions: " + exs);
}
La seule façon possible que j'imagine est de mapper les valeurs d'une liste à une monade, qui représentera le résultat de l'exécution de votre traitement (succès avec valeur ou échec avec jetable). Et puis pliez votre flux en un seul résultat avec une liste agrégée de valeurs ou une exception avec une liste de valeurs supprimées des étapes précédentes.
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)
L'implémentation des résultats peut varier mais je pense que vous avez compris.