Java Generics Hell - ジェネリックな例外

Java Generics Hell アドベントカレンダー 18日目。

読者の推奨スキルとしてはOCJP Silverぐらいを想定している。

throws E

Javaジェネリクスの型変数は例外のthrows宣言でも用いることができる。型変数の宣言時にthrow可能な型であることを型変数の境界で示す必要がある。

public <E extends Exception> void hoge() throws E {}

上記はメソッドスコープの型変数 E を extends Exception としてみた。メソッドスコープの型変数だとthrowsに型変数を用いる意味があまりないが、型システムの挙動を見る分にはコード量が少なくて済むので都合が良い。

バインドの仕方でthrowされうる例外が変わる、例外が変わるのでcatchするべき例外も変わる。

// IOExceptionをバインド
try {
	this.<IOException>hoge();
} catch (IOException e) {
	// 略
}
// SQLExceptionをバインド
try {
	this.<SQLException>hoge();
} catch (SQLException e) {
	// 略
}
// RuntimeExceptionをバインド
// catch不要
this.<RuntimeException>hoge();

面白いのはRuntimeExceptionをバインドした場合は、catch不要となることだ。

AutoCloseableの例

ここで、java.lang.AutoCloseableを見てみよう。通常、メソッドのthrowsは具象型が用いられる。

public interface AutoCloseable {
    void close() throws Exception;
}

例えばjava.io.ByteArrayInputStreamでは

public class ByteArrayInputStream extends InputStream {
    // 略

    public void close() throws IOException {
    }
}

となっていて、ByteArrayInputStreamの具象型を扱っていてもclose()を呼ぶ際にIOExceptionが発生する扱いとなる。

byte[] buf = new byte[100];
try (ByteArrayInputStream bais = new ByteArrayInputStream(buf)) {
	// 略
} catch (IOException e) {
	// close() が throws IOException のため
}

throws Exceptionではなくthrows IOExceptionなのは、例外がメソッドからの出力であるため、より狭い、より具体的な型に絞っても良いからである。このあたりはジェネリクス以前からのJavaの型システムの機能性だが、ジェネリクスワイルドカードの話にも似ている。

というか、オーバーライドしてthrowsなくしておけばよかったじゃないか、という話もあるんだが、APIの歴史的な後の祭り。Java 1.0 のときに thrwos IOExceptionとしてしまったので、これをなくすとthrowされないIOEexceptionをcatchするコードがコンパイルエラーになってしまうので残されているのである。Javaは到達不能コードをコンパイルエラーにする方針なのでしょうがない。

話がそれた。

ここで言っておきたいのは、AutoCloseable型変数に一度代入してしまえば、そのclose()はthrows Exceptionだということだ。

byte[] buf = new byte[100];
ByteArrayInputStream bais = new ByteArrayInputStream(buf);
try (AutoCloseable ac = bais) {
	// 略
} catch (Exception e) {
	// IOException ではなく Exception になる
}

この点がジェネリクスなしでの不都合ということになる。

例外をインスタンス型変数にした場合

ではここにAutoCloseableとの対比としてinterface Foo を以下のように定めてみよう。

public interface Foo<E extends Exception> {
	void foo() throws E;
}

これをimplementsするclassは以下のようになる。

public class Bar implements Foo<IOException> {
	@Override
	public void foo() throws IOException {}
}

オーバーライドするにあたって throws IOException となった。
これでパラメタライズドタイプを用いて以下のように書ける。

Bar bar = new Bar();
Foo<IOException> foo = bar;

try {
    foo.foo();
} catch (IOException e) {
    // IOExceptionで済む
}

とはいえ、Fooがパラメタライズドタイプになって鬱陶しい。

なお、Foo<RuntimeException>であればcatch不要となる。

まとめ

例外のthrows宣言に型変数を用いることができるが、正直、あんまり活用できるポイントはない。