Java: try-with-resources で catch 節を使う

Java 7 で追加された try-with-resources は C# の using ステートメントに倣った構文だが、まったく同じというわけではない。
C# の using ステートメントは下記のページで説明されているように try/finally の糖衣構文であって、それ以上でもそれ以下でもない。

using ステートメント (C# リファレンス)
http://msdn.microsoft.com/ja-jp/library/yh598w02.aspx

一方、Java の try-with-resources では、catch 節を書くことができる。これは大きな違いのように思えるのだけど、Web 上で両者を比較しているページを見ても、触れられていないことが多い……ような気がする。

復習

catch 節を付けた場合について考える前に、付けない場合の例外処理について考えることにする。try-with-resources の典型的なサンプルコードは次のようなものだ。

		try (InputStream in = new ContraryInputStream()) {
			in.read();
		}

ここで使っている ContraryInputStream は、いま手元で作った、次のようなクラスだ。read() メソッドを呼ぶといきなり例外を投げてくる、天の邪鬼な (contrary) 入力ストリーム。

class ContraryInputStream extends InputStream {
	@Override
	public int read() throws IOException {
		throw new IOException("よめませーん");
	}
}

この場合、read() で例外が発生したことによって try ブロックを抜けることになり、抜ける前にリソースである ContraryInputStream の close() メソッドを呼んでくれる。基本ですね。パターン 1 と呼ぶことにする。ここで先ほどのサンプルコードを動かし、try ブロックの外側に送られる例外のスタックトレースを出力すると、次のようになっている。

java.io.IOException: よめませーん
	at lab.ContraryInputStream.read(TryWithResourcesDemo.java:31)
	at lab.TryWithResourcesDemo.main(TryWithResourcesDemo.java:18)

では、次にパターン 2 として、read() ではなく、close() が失敗した場合を考える。ContraryInputStream を次のように書き換えてみる。

class ContraryInputStream extends InputStream {
	@Override
	public int read() throws IOException {
		return 0;
	}
	
	@Override
	public void close() throws IOException {
		throw new IOException("とじれませーん");
	}
}

この場合のスタックトレースも、Java 6 までとほとんど変わらない。

Exception in thread "main" java.io.IOException: とじれませーん
	at lab.ContraryInputStream.close(TryWithResourcesDemo.java:40)
	at lab.TryWithResourcesDemo.main(TryWithResourcesDemo.java:19)

少し様子が変わるのは、ブロック内と close() の両方で例外が発生した場合だ。これをパターン 3 とする。再び ContraryInputStream を次のように書き換えて、スタックトレースを確認してみよう。

class ContraryInputStream extends InputStream {
	@Override
	public int read() throws IOException {
		throw new IOException("よめませーん");
	}
	
	@Override
	public void close() throws IOException {
		throw new IOException("とじれませーん");
	}
}

スタックトレースをコンソールに出力すると、次のようになる。"Suppressed" というキーワードの後に、close() で発生した例外のスタックトレースが、インデント付きで出力されている。

Exception in thread "main" java.io.IOException: よめませーん
	at lab.ContraryInputStream.read(TryWithResourcesDemo.java:34)
	at lab.TryWithResourcesDemo.main(TryWithResourcesDemo.java:18)
	Suppressed: java.io.IOException: とじれませーん
		at lab.ContraryInputStream.close(TryWithResourcesDemo.java:40)
		at lab.TryWithResourcesDemo.main(TryWithResourcesDemo.java:19)

ここまでが、catch 節がない場合。

catch 節がある場合の挙動

catch 節付きで try-with-resources を使う場合のサンプルコードは次のようになる。

		try (InputStream in = new ContraryInputStream()) {
			in.read();
		} catch (IOException e) {
			e.printStackTrace();
		}

ここではスタックトレースを出力しているだけだが、もちろん他のことをやってもいい。普通の try-catch-finally の catch でできるように、一度 catch した例外を再度 throw してもいいし、例外の型に応じて処理を分けたければ catch 節を複数書くこともできる。が、この記事ではとりあえず上記のサンプルコードを使うことにして、catch 節なしの場合と同じように 3 つのパターンを調べてみることにする。
パターン 1 では、驚くようなことは起きない。read() で例外が起きると catch 節が実行され、次のようなスタックトレースが出力される。

java.io.IOException: よめませーん
	at lab.ContraryInputStream.read(TryWithResourcesDemo.java:33)
	at lab.TryWithResourcesDemo.main(TryWithResourcesDemo.java:22)

ところが、パターン 2、つまり close() で例外が起きるパターンでも、catch 節に入ってスタックトレースが出力される。

java.io.IOException: とじれませーん
	at lab.ContraryInputStream.close(TryWithResourcesDemo.java:39)
	at lab.TryWithResourcesDemo.main(TryWithResourcesDemo.java:23)

この挙動は、try-with-resources を、従来の try-catch-finally から finally を省略したようなものと思い込んでいると、少し不思議に思える。現実の try-with-resources に付けた catch 節は、try ブロック内と close() のどちらで例外が起きた場合にも実行されるのだ。
最後のパターン 3 はどうか。

java.io.IOException: よめませーん
	at lab.ContraryInputStream.read(TryWithResourcesDemo.java:33)
	at lab.TryWithResourcesDemo.main(TryWithResourcesDemo.java:22)
	Suppressed: java.io.IOException: とじれませーん
		at lab.ContraryInputStream.close(TryWithResourcesDemo.java:39)
		at lab.TryWithResourcesDemo.main(TryWithResourcesDemo.java:23)

ここでもやはり catch 節が実行され、スタックトレースが出力されるが、それは 1 回だけである。例外が 2 回起きたからといって、catch 節が 2 回実行されることにはならない。
Java 言語仕様の記述 (14.20.3.) によれば、catch 節付きの try-with-resources は、catch 節なしの try-with-resources の外側に従来の try-catch を書いたのと同じことになるらしい。すなわち、先ほどのサンプルコードは、次のように書いたのと同じことになるのだという。そう考えると、パターン 2 と 3 が上述のような結果になったのも理解できる。

		try {
			try (InputStream in = new ContraryInputStream()) {
				in.read();
			}
		} catch (IOException e) {
			e.printStackTrace();
		}

この記事はすでに長文になっているのでもう触れないが、try-with-resources に finally 節を付けることもできる。この場合も、catch 節の場合と同じように、catch 節なしの try-with-resources を従来の try-finally で囲ったのと同じことになる。さらに、try-with-resources で catch 節と finally 節の両方を書いた場合についても、同じ考え方となる。

catch 節付き try-with-resources の注意点 その 1

ここまで見てきたように、try-with-resources の catch 節は常に、close() が呼ばれた後に実行される。
多くの場合はこれで問題なくうまくいく。よく考えられた仕組みだと思う。
ただし、close() より後であるということは、従来の catch 節では参照できたはずの値が参照できない可能性もある、という点に注意がいるかもしれない。
例として、Java 6 の時代に次のようなコードが書かれていたとする。

		InputStream in = null;
		try {
			in = new ContraryInputStream();

			int b;
			while ((b = in.read()) != -1) {
				doSomething(b);
			}
		} catch (IOException e) {
			LOG.error(
					"読み取りに失敗しました。あと" + in.available() + "バイトくらいは読めそうでしたが諦めます",
					e);
		} finally {
			if (in != null) {
				try {
					in.close();
				} catch (IOException e) {
					LOG.error("クローズに失敗しました", e);
				}
			}
		}

ある日、Java 7 に移行しようということになり、リファクタリングのつもりでコードを次のように修正しようとする。

		try (InputStream in = new ContraryInputStream()) {
			int b;
			while ((b = in.read()) != -1) {
				doSomething(b);
			}
		} catch (IOException e) {
			LOG.error(
					"読み取りに失敗しました。あと" + in.available() + "バイトくらいは読めそうでしたが諦めます",
					e);
		}

ところがこのコードはコンパイルできない。先ほどの説明を思い出してほしい。catch 節付きの try-with-resources がどのように変換されるかを。

		try {
			try (InputStream in = new ContraryInputStream()) {
				int b;
				while ((b = in.read()) != -1) {
					doSomething(b);
				}
			}
		} catch (IOException e) {
			LOG.error(
					"読み取りに失敗しました。あと" + in.available() + "バイトくらいは読めそうでしたが諦めます",
					e);
		}

変数 in のスコープ (有効範囲) は内側の try-with-resources の中だけなので、その外の catch 節では参照できないのだ。

catch 節付き try-with-resources の注意点 その 2

コンパイルできないなら、原因がわからなくても首をひねりながら元のコードに戻せばいいのだから、コンパイルできてしまうよりはよさそうだ。
ただし、catch 節の実行が close() より後だということに気付かないと、他にも妙なところで罠を踏んでしまうかもしれない。
僕は以前の記事で、try-with-resources を使って、Log4j の MDC を管理できないか、ということを書いた。

www.toyfish.blog

		try (MDCResource mdc = MDCResource.put("id", id)) {
			LOG.info("処理中だよー! ほんとだよー!!");
		}

Log4j では、ある処理の最初に MDC.put("id", id); と書いて最後に MDC.remove("id") とやると、その処理の間だけログ出力用の変数 (のようなもの) に値をセットして、ログに毎行出力させることができる。ただ、うっかりすると remove せずに処理を抜けてしまったりするので、try-with-resources を使って上記のように書けたらいいんじゃないか、というような内容だった。
しかしこのアイデアは、次のように catch 節を組み合わせた時点で、期待とは異なる動きをすることになる。

		try (InputStream in = new ContraryInputStream();
				MDCResource mdc = MDCResource.put("id", id)) {
			LOG.info("処理中だよー! ほんとだよー!!");
			in.read();

		} catch (IOException e) {
			LOG.error("失敗しましたてーきーなー?", e);
		}

しつこいようだが、catch 節に入るのは close() より後なので、上のように書いてしまうと、catch 節内でログを出した時点ではすでに MDC の値が消されてしまっている。
この場合、コンパイルは通ってしまうし、JUnit なんかのテストケースでも MDC の値がちゃんと出力されているかまではチェックしていないことが多いだろう。幸い、このアイデアを仕事で使ったことはないが、もし使っていたら……リリースして運用を初めて、ある日例外が起きたときに、ログを見ても値が出力されておらず、途方に暮れることになる。恐ろしいことである。