ラムダ関数の一般的な例外

UPD:標準ストリームの上に遅延計算を使用した例を追加しました。



ご存じのとおり、Stream APIの機能インターフェイスから制御されたAPIをスローすることはできません。 何らかの理由でこれが必要な場合(たとえば、ファイル、データベース、またはネットワーク経由で作業する場合)、RuntimeExceptionでそれらをラップする必要があります。 これは、エラーが無視される場合はうまく機能しますが、エラーを処理する必要がある場合、コードは扱いにくく読みにくいものです。 私は、一般的な例外を使用してインターフェイスとメソッドを宣言できるかどうかに興味を持ち、予想外に自分に可能なことを見つけました。



このような機能的インターフェイスを定義します。標準インターフェイスのFunction <A、B>とは、スローされた例外の3番目のジェネリック型がある場合のみ異なります。



public interface FunctionWithExceptions<A, B, T extends Throwable>{ B apply(A a) throws T; }
      
      





そして、このインターフェイスを使用してコレクションを変換する単純なメソッドを宣言します。このメソッドには、スローされた例外の汎用タイプもあります(機能インターフェイスがスローできる例外のタイプに一致します)。



 public static <A, B, T extends Throwable> Collection<B> map(Collection<A> source, FunctionWithExceptions<A, B, T> function) throws T { Collection<B> result = new ArrayList<>(); for (A a : source) { result.add(function.apply(a)); } return result; }
      
      





例外の処理が異なるケースでどのように見えるかを見てみましょう。



1つの例外



スローされる例外をスローするラムダ関数を使用してコレクションを変換してみましょう。ジェネリック型は、マップメソッド呼び出しの場所に正しく渡されるためです。 この場合、例外のタイプが保存されます。



 public Collection<byte[]> singleException(Collection<String> filenames) throws IOException { return map(filenames, f -> Files.readAllBytes(new File(f).toPath()); }
      
      





1つのラムダ関数の2つの例外



処理されたいくつかの例外をスローする関数を使用すると、それらは最も一般的な型にキャストされますが、これはあまり良くありません(ただし、RuntimeExceptionで例外をラップするよりも悪くありません)。



 public static byte[] waitAndRead(String filename, long time) throws InterruptedException, IOException { Thread.sleep(time); return Files.readAllBytes(new File(filename).toPath()); } public Collection<byte[]> joinedExceptions(Collection<String> filenames) throws Exception { return map(filenames, f -> waitAndRead(f, 1000L)); }
      
      





さまざまなラムダ関数の例外



ただし、それぞれが1つ以下の例外をスローするラムダ関数のチェーンを記述する場合、もちろんそれらは結合されず、個別に正しく処理できます。



 private <T> T wait(T t, long time) throws InterruptedException { Thread.sleep(time); return t; } private byte[] read(String filename) throws IOException { return Files.readAllBytes(new File(filename).toPath()); } public Collection<byte[]> separatedExceptions(Collection<String> filenames) throws InterruptedException { try { return map(map(filenames, f -> wait(f, 1000L)), f -> read(f)); } catch (IOException e) { return Collections.emptyList(); } }
      
      





この例でわかるように、IOExceptionで空のコレクションをキャッチして返し、上記のInterruptedExceptionを渡します。



例外のないラムダ関数



最後に、制御された例外をスローしない関数の動作を見てみましょう。そうでない例外を処理する必要がありますか?



 public Collection<Boolean> noExceptions(Collection<String> filenames) { return Mapper.map(filenames, f -> new File(f).exists()); }
      
      





すべてがうまく機能し、例外を処理する必要はありません。 この場合、Genericタイプの例外がRuntimeExceptionで自動的に検出されたことは興味深いことです。これは原則として論理的ですが、少し予想外です。



短所



上記のアプローチの主な欠点は、標準APIの代わりに一般的な例外を持つインターフェイスを使用できないため、Stream APIとの非互換性です。 潜在的に、StreamExと同様のThrowableStream APIを記述したり、StreamExを拡張したりできますが、これには多くの簡単なコードを記述する必要があります。 2番目の欠点は、複数の汎用例外を宣言できないことです。



ところで、Javaの以前のバージョン(1.7でチェック)のジェネリック型を持つクラスで例外を使用できますが、それは不便であり、したがって無意味です。



UPD:

レイジーコンピューティング



遅延計算をサポートするために、標準ストリームの周りにこのようなラッパーを作成します(たとえば、2つのメソッドのみ、残りは同様の方法で実装されます)。



 public class ErStream<S, T extends Throwable> { private final Stream<S> mainStream; public ErStream(Stream<S> mainStream) { this.mainStream = mainStream; } public static <S, R, T extends Throwable, T1 extends T, T2 extends T> ErStream<R, T> map(ErStream<S, T1> erStream, FunctionWithExceptions<S, R, T2> function) { Function<S, R> f = uncheck(function); return new ErStream<>(erStream.mainStream.map(f)); } public static <S, T extends Throwable> Optional<S> findAny(ErStream<S, T> erStream) throws T { return erStream.mainStream.findAny(); } //  Djaler private static <S, R> Function<S, R> uncheck(FunctionWithExceptions<S, R, ?> function) { return t -> { try { return function.apply(t); } catch (Throwable exception) { throwAsUnchecked(exception); return null; } }; } //  Djaler @SuppressWarnings("unchecked") private static <E extends Throwable> void throwAsUnchecked(Throwable exception) throws E { throw (E) exception; }
      
      





そして、次のように使用します(簡潔にするために、すべての例は1つのコードブロックに含まれています)。



 private static byte[] read(String filename) throws EOFException { return null; } private static String search(String filename) throws FileNotFoundException { return filename; } public static void main(String[] args) { List<String> list = Arrays.asList("1.txt"); //autogenerated code with wrong generic types //ErStream<Object, Throwable> temp = map(new ErStream<>(list.stream()), s -> search(s)); //store single exception ErStream<String, FileNotFoundException> temp = map(new ErStream<>(list.stream()), s -> search(s)); try {//exception will be thrown here findAny(temp); } catch (FileNotFoundException e) {/*NOTHING*/} temp = map(new ErStream<>(list.stream()), s -> search(s)); try {//most general exception will be thrown here findAny(map(temp,s -> read(s))); } catch (IOException e) {/*NOTHING*/} //without exception, nothing thrown findAny(map(new ErStream<>(list.stream()), i -> i + 1)); }
      
      





この例では、エラーは端末操作からスローされ、ErStreamは変数に保存できます(残念ながら、Eclipse 4.6.3では、正しいジェネリック型の代わりに最も一般的な<Object、Throwable>型が表示され、別の関数に転送する必要があります)。 また、静的メソッドでのみいくつかの例外を結合するための共通の型を導出することも適切でした。 また、エラーを生成しないErStreamをローカル変数に保存する場合、エラーの一般的なタイプはRuntimeExceptionになり、他のエラーと組み合わせると、一般的なタイプを例外として出力します(関数をチェーンに書き込むときにこのような問題はありません)。



ソースコードgitテストにリンクして、記事に記載されている状況と同様の状況を確認します。



All Articles