もぐてっく

人は1つ歳をとるたび、1ビットづつ大きくなれると信じてた。

木曜スペシャル!アパッチの秘宝!Commonsの奥地で伝説の「例外が投げられるStream」を見た!

Javaお前な。そういうとこやぞ。

いやぁ。JavaのStreamって便利ですね!

forループのあるあるバグ要素を排して、メソッドチェーンでどんどん処理がつなげられる。
書いててとても気持ちのいい!*1rubyとかJavascriptにもあるアレです。

しかしながらこのStream野郎。例外との相性が悪いと言う軽い致命傷があります。
Streamで使ってる関数インターフェース(ConsumerとかFunction)にthrowsが付いてないので、その中身で例外を処理することが要求されるんですね。

そのため、Stream処理の途中で例外を吐いて以降の処理を止めたりできないとか、Streamの外で例外処理を行うのが面倒だったりとか。
結局例外を吐くループ処理は従来の拡張forを使うみたいな残念な状況になってます。

そんなある日。

たまたまApache財団のJava便利ライブラリCommonsを探索していたらFailableStreamというクラスを発見。

「失敗可能なStream」ってもしかしてこれアレじゃないの?と思ってJavadocを見るとlambda内で例外をキャッチしなくてもいいStreamらしい。
ってことは当然キャッチしなかった例外を外に伝搬できるってことだよね???もっと調べてみよう!

と思ったけどそれ以上の説明はなし。
ググってみるけど気持ち悪いくらい誰もこのクラスについて言及をしていない。

と言うわけで今回は、私もぐのが体を張ってこのクラスをご紹介していこうと思います。

とりあえず試してみよう!

素のStreamと例外処理のおさらい

まずは比較対象として素のJavaで今回書きたいコードを書いてみます。
Streamのmap()のlambdaで例外をthrowして、map()の外側でそれをキャッチしたい感じです。

しかし、mapに渡すFunctionインターフェースのapply()メソッドにはthrowsが付いてないので、lambda内で例外のキャッチを求められます。

package com.example.demox;

import java.io.IOException;

import static java.util.stream.Collectors.toList;

import java.util.stream.Stream;

public class DemoxApplication {

	public static void main(String[] args) {
		try {
			Stream.of(0).map(a -> {
				throw new IOException("test");
						 	↑
						ここでコンパイルエラー
			}).collect(toList());
		} catch (Exception e) {
			System.out.println(e.getMessage());
		}
	}
}

Streamの外で例外の発生有無を確認するには、Stream外のListに例外オブジェクトをメモるとか何かしらの工夫が必要です。

package com.example.demox;

import java.io.IOException;

import static java.util.stream.Collectors.toList;

import java.util.stream.Stream;

public class DemoxApplication {

	public static void main(String[] args) {
		List<Exception> exceptions = new ArrayList<>();

		Stream.of(0).map(a -> {
			try {
				throw new IOException("test");
			} catch (Exception e) {
				System.out.println(e.getMessage());
				exceptions.add(e);
			}
		}).collect(toList());

		// ここでexceptionsを評価
	}
}

FailableStreamと例外処理

それでは今回のメインイベント。FailableStream。

このmap()はFailableFunctionって言うthrows付きのメソッドを持ってるインターフェースを引数に取るのでコンパイルエラーは発生しませんね。
期待できます。

package com.example.demox;

import java.io.IOException;

import static java.util.stream.Collectors.toList;

import java.util.stream.Stream;

import org.apache.commons.lang3.function.Failable;

public class DemoxApplication {

	public static void main(String[] args) {
		try {
			Failable.stream(Stream.of(0)).map(a -> {
				throw new IOException("これはテスト用の例外です!もぐののダジャレは冷害と界隈からよく言われていますが。そんなことないと思いますよプンプン。");
			}).collect(toList());
		} catch (Exception e) {
			System.out.println(e.getMessage());
		}
	}
}

それでは実行。

ちゃんとlamda内でthrowしたIOExceptionがキャッチできて、僕の「例外」「冷害」を掛けた渾身のメッセージがコンソー

null

も「虚無・・・やと?」

雲行きが怪しくなってくる

言われてみればこのFailableStream。map()にthrows付いてないんすよね。
素直に例外を受け取ってる訳では無い様です。

何が起こってんのかデバッガで見てみます。
なんかリフレクション関連のけったいな例外UndeclaredThrowableExceptionが出てますね。

f:id:moguno:20210211101034p:plain

一般的にネストした例外を突っ込むフィールドであるcauseもnull。
独自のundeclaredThrowableってフィールドに僕が放った渾身のExceptionが格納されてます。

ドキュメントを適当に読んでみます。

UndeclaredThrowableException (Java Platform SE 6)

呼び出しハンドラの invoke メソッドが、プロキシインスタンスで呼び出され呼び出しハンドラにディスパッチされたメソッドの throws 節で宣言されたどの例外タイプにも割り当てできない確認済み例外 (RuntimeException または Error に割り当てできない Throwable) をスローした場合、プロキシインスタンスのメソッド呼び出しによってスローされます。

UndeclaredThrowableException インスタンスは、呼び出しハンドラによってスローされた、宣言されていない確認済み例外を格納しており、getUndeclaredThrowable() メソッドで取り出すことができます。UndeclaredThrowableException は RuntimeException を拡張するため、確認済み例外をラップする未確認例外となります。

も「なるほど120%理解した(目にハイライトなし)」

まず「未確認例外」って言うのは、いわゆる非検査例外のことと解釈。
(検索したけどこのクラスの説明以外にヒットせず)

確か非検査例外ならば普通のStreamの外でも例外が受け取れるって言うハックがあるはず。それを利用してるのかしら。

次、リフレクション関係の例外ってことはFunction::apply()をリフレクション呼び出ししてんのかなぁ?とFailableFunctionのソースを追うもリフレクションでapply()を呼び出してる箇所は発見できず。

まぁ、JVMが関数インターフェースの唯一のメソッドを特定する処理とかでリフレクションしてそうだからそんなもんなんでしょう。

大体(空気間は)わかったけど、UndeclaredThrowableExceptionをキャッチしないと真の例外が見れないのは可読性下がりそうでやだなぁ。
と思ってたら、

1.4 リリースでは、この例外は汎用的な例外チェーン機構に適合するように改良されています。「呼び出しハンドラによりスローされた宣言されていない確認済み例外」 (構築時にスローされ、getUndeclaredThrowable() メソッドを介してアクセス可能) は、cause メソッドと呼ばれるようになり、前述の「レガシーメソッド」に加えて Throwable.getCause() メソッドを介してアクセス可能です。

あ、getCause()で取れるのね。ギリ許せるか。

これが決定打か?

最終的にたどり着いたのがこのコード。

import static java.util.stream.Collectors.toList;

import java.io.IOException;
import java.util.stream.Stream;

import org.apache.commons.lang3.function.Failable;

public class DemoxApplication {

	public static void main(String[] args) {

		try {
			Failable.stream(Stream.of(0)).map(a -> {
				throw new IOException("これはテスト用の例外です!もぐののダジャレは冷害と界隈からよく言われていますが。そんなことないと思いますよプンプン。");
			}).collect(toList());
		} catch (Exception e) {
			if (e.getCause() instanceof IOException) {
				System.out.println(e.getCause().getMessage());
			}
		}
	}
}

catchの中が例外もみ消し事故を誘発しそうでちょっと使いにくいなぁ。。。

引き続き、もっといい銀の弾丸を探そうと思います。

【このブログがお気に召しましたら、ぜひ以下のリツイートをお願いします!】

*1:後から見ると可読性が死んでたりしますが