ランタイム例外をスローするメソッドがあるとしましょう。 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 ..)など、これをハッキングするいくつかの方法を考えることができます。しかし、これを行うネイティブな方法はありますか?そうでない場合、きれいに実装する別の方法はありますか?
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);
});
}
Java用のTry
モナドの実装が既にいくつかあります。たとえば、 better-Java8-monads libraryを見つけました。これを使用して、次のスタイルで記述できます。
値をマップし、すべての例外を追跡するとします。
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 は完全な例です。
例外へのマッピングというテーマのバリエーションを次に示します。
既存の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);
}
私が想像できる唯一の可能な方法は、リスト内の値をモナドにマッピングすることです。これは、処理実行の結果(値で成功またはスロー可能で失敗)を表します。次に、ストリームを、値の集計リストを使用して単一の結果に折り畳むか、前の手順で抑制されたリストを使用して1つの例外を作成します。
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)
結果の実装は異なる場合がありますが、アイデアは得られると思います。