ProjectLambda・4部(例外ジェネリクス)
例外ジェネリクスはProject Lambdaでは導入されません。*1
一応、将来的に導入された場合のことを考えて記事自体は残しておきます。
ProjectLambdaで導入される言語的な変更は今から説明する「例外ジェネリクス」で最後です。
例外ジェネリクスはラムダ式と直接的な関係は無いんですが、一応ProjectLambdaの範囲です。(汎用的な関数型インターフェースを宣言するときに必要になってくる)
例外ジェネリクスの具体的な説明に入る前に、現在の例外の型システムの問題点を見ておきましょう。
例えば、次のような汎用的なインターフェースCallableがあるとします。
interface Callable { void call(); }
この場合Callable#callは検査例外を投げません。
なのでこれを実装したクラスはcallから検査例外を投げることが出来ません。
これを解決する方法としては、1.「握りつぶす」、2.「非検査例外に落とし込んで投げる」、3.「定義を変更する」などが代表的な対策として存在すると思います。
1, 2, 3はcall内で検査例外が投げられるような処理があり、それを呼び出し元に報告する必要があるというシチュエーションです。
2, 3はcall内から何らかの新しい検査例外を投げるというシチュエーションです。
1.「握りつぶす」
これが一番楽・簡単な方法でしょう
class C implements Callable { public void call() { try (InputStream is = ...) { is.read(); } catch (IOException e) {} } }
「握りつぶす」が妥当な処理であれば結構かもしれませんが、ほとんどの場合呼び出し元に何らかのメッセージを伝えるべきでアンチパターンに分類されます。
2.「非検査例外に落とし込んで投げる」
class C implements Callable { public void call() { try (InputStream is = ...) { if (is.read() == 0) { // ホントは検査例外を投げたいんだけど・・・ throw new MyRuntimeException(); } } catch (IOException e) { throw new RuntimeIOException(e); } } }
RuntimeIOExceptionはIOExceptionの非検査版、MyRuntimeExceptionは本当は投げたい検査例外の非検査版だと思ってください。
こうすることで呼び出し元に例外を伝えることが出来ますが、どこかで必ずキャッチされるべき検査例外を非検査例外に落とし込むというのは検査例外の意味・重みを失わせます。
多くの場合、これもアンチパターンに分類されます。
少なくとも苦し紛れで美しくありません。
3.「インターフェースの定義を変更する」
Callable#callが検査例外を投げれない定義なのであれば、検査例外を投げることができる定義に変更すれば良いはずです。
interface Callable { void call() throws Exception; }
この様にすれば実装クラスにおいて検査例外を投げることが出来ます。
class C implements Callable { public void call() throws IOException { try (InputStream is = ...) { if (is.read() == 0) { throw new MyException(); } } } }
これで一件落着の様に見えますが、それは例外を投げる箇所においてだけです。
というのも、このクラスを実際に使うときに別の問題が発生してしまいます。
Callable c = new C(); try { c.call(); } catch (Exception e) { // 例外処理 }
Exceptionと全例外を受け取っているのは問題です。
本来ここは限定してIOExceptionとして受け取るべきです。
今回の場合はCとしてCのインスタンスを受け取ればよいかもしれませんがCallableを受け取るメソッドがある場合はどうでしょう?
class Processer { public void doProcess(Callable callable) throws Exception { callable.call(); } } class C1 implements Callable { public void call() throws IOException { throw new IOException(); } } class C2 implements Callable { public void call() throws MyException { throw new MyException(); } } public class Main { public static void main(String[] args) { Processer p = new Processer(); try { p.doProccess(new C1()) } catch (Exception e) {} // 本当はC1はIOExceptionを投げるのに・・・ try { p.doProccess(new C2()); } catch (Exception e) {} // 本当はC2はMyExceptionを投げるのに・・・ } }
本来はdoProccessで実際に投げられる物だけをdoProcessのthrows節に書くべきです。
そこでCallable#callが投げる例外をジェネリクスを用いて書いてみましょう。
interface Callable<E extends Exception> { void call() throws E; } class Proccesser { public <E extends Exception> void doProccess(Callable<E> c) throws E { c.call(); } } class C1 implements Callable<IOException> { public void call() throws IOException { throw new IOException(); } } class C2 implements Callable<MyException> { public void call() throws MyException { throw new MyException(); } } public class Main { public static void main(String[] args) { Proccesser p = new Proccesser(); try { p.doProccess(new C1()); p.doProccess(new C2()); } catch (IOException | MyException e) { // ちゃんと必要な分だけ受け取れた } } }
これで一歩解決に近づきました。
まだ、一歩です。これで解決とはなりません。
次のクラスを定めるときCallableの型引数へは何を渡せばよいでしょう?
public class C implements Callable<???> { private boolean state; public void call() throws IOException, MyException { if (state) { throw new IOException(); } else { throw new MyException(); } } }
callはIOExceptionとMyExceptionを投げます。
ただ、実型引数に複数のクラスを指定することはできません。
そのためこれをコンパイルするにはCallableの型引数にExceptionを渡すほかありません。(MyExceptionはIOExceptionを継承していないとする)
これでは結局元通りです。
例外ジェネリクス
そこで登場するのが新しいジェネリクス、例外ジェネリクスです。
例外ジェネリクスの仮引数は通常のジェネリクスと同じところに書き、「throws 型引数」の形で書きます。
interface Callable<throws E> { void call() throws E; <throws E1> void method() throws E, E1; // 当然メソッドやコンストラクタのジェネリクスにも使用できる }
実引数へは普通のジェネリクスと同じように「例外クラス」と書きます。
この例外クラスはExceptionのサブクラスである必要があります。
class C implements Callable<IOException> { public void call() throws IOException {} public <throws E1> void method() throws IOException, E1 {} } class Main { public static void main(String[] args) { Callable<IOException> c = new C(); try { c.call(); c.<MyException>method(); } catch (IOException | MyException e) {} } }
これだけでは先ほどと変わりませんが、例外ジェネリクスの実引数へは複数のクラスを指定することが出来ます。
複数指定するには「A | B」と|で繋ぎます。(複数の型境界を指定する&やマルチキャッチの|を思い出させますね。)
class C implements Callable<IOException | MyException> { public void call() throws IOException, MyException {} public <throws E> void method() throws IOException, MyException, E {} } class Main { public static <throws E> void doProccess(Callable<E> c) throws E { c.call(); } public static void main(String[] args) { try { doProccess(new C()); // doProcessの型引数へは型推論よりIOException | MyExceptionが渡される。 } catch (IOException | MyException e) {} } }
どうでしょう?これで一件落着です。
何も例外を投げない場合は例外ジェネリクスの実引数に「void」を指定します。
class C implements Callable<void> { public void call() {} // 何も検査例外を投げない public <throws E> void method() throws E {} public static void main(String[] args) { Callable<void> = new C(); c.call(); c.<void>method(); } }
例外ジェネリクスの型引数でキャッチすることも出来ます。
public <throws E> void method(Callable<E> callable) { try { callable.call(); } catch (E e) {} }
例外ジェネリクスの出来る事出来ない事
ここから先は普通のジェネリクスと例外ジェネリクスとを比較しつつ出来る事・出来ない事をまとめてみます。
[出来ない事]変数や引数などの型として使用
例外ジェネリクスの型引数を普通のジェネリクスと同じように変数や引数などの型として使用することはできません。
public class C<throws E> { private E e; // NG @SuppressWarnings("unchecked") public C(E e) // NG { this.e = (E)e; // NG } public E get() // NG { return e; } }
[出来ない事]例外ジェネリクスの型引数を普通のジェネリクスの実引数に使用する
public class C<T extends Exception> { public static <throws E> C<E> create() // NG { C<E> c = new C<E>(); // NG return c; } }
そもそも、これら二つが出来るとすると
C.<IOException | MyException>create();
みたいなことが出来て普通のジェネリクスでは解釈不可能なことが出来てしまうので不可能で当然。
[出来る事]型境界指定
普通のジェネリクス同様型境界を指定することが出来ます。
指定する際も同じように型引数のあとに「extends 型」と書きます。
public class C<throws E extends IOException> // OK { public <throws E extends IOException> C() {} // OK public <throws E1 extends IOException, throws E2 extends E> void method() {} // OK }
普通のジェネリクスで型境界が指定されなかった場合、その型引数はObjectとして型イレイジャされます。
public class C<T> { public T method(T t) { return null; } } // 型イレイジャ後 public class C { public Object method(Object t) { return null; } }
これと同じように例外ジェネリクスで型境界が指定されなかった場合、その型引数はExceptionとして型イレイジャされます。
public class C<throws E> { public void method() throws E {} } // 型イレイジャ後 public class C { public void method() throws Exception {} }
→複数の例外ジェネリクスの型引数を使用してcatchする場合の注意
[まとめ]例外ジェネリクスの型引数を使用できる場所
例外ジェネリクスの型引数を使用できるのは次の場所のみです。
1.throws節
2.catch節
3.例外ジェネリクスの実引数
4.例外ジェネリクスの仮引数の境界[2012/03/28:削除:仮引数の境界には使用できませんでした]
[出来る事]普通のジェネリクスの型引数を例外ジェネリクスの実引数に使用する
互換性がある場合のみ出来ます。
public class C<throws E> { public static <T extends Exception> C<T> create() // OK { return new C<T>(); // OK } }
[出来ない事]&でつなぐ型境界指定
そもそも例外ジェネリクスはthrows節の定義の強化を目的としているのでそれ以上の事は出来ません。
public interface I { public void m(); } public class C<throws E extends Exception & I> // NG { public void method() throws E {} public void invoke() { try { this.method(); } catch (E e) { e.m(); // もし&が許されたらこういったことが出来る } } }
[出来る事]ワイルドカード
普通のジェネリクスと同じように利用できます。
public class C<throws E> { public void method() throws E {} public static void main(String[] args) { C<?> c1 = new C<IOException>(); // OK C<? extends IOException> c2 = new C<IOException>(); // OK C<? super IOException> c3 = new C<IOException>(); // OK invoke1(c1); invoke2(c2); invoke3(c3); } public static void invoke1(C<?> c) // OK { try { c.method(); } catch (Exception e) {} } public static <throws E> void invoke2(C<? extendsE> c) // OK { try { c.method(); } catch (E e) {} } public static <throws E> void invoke3(C<? super E> c) // OK { try { c.method(); } catch (Exception e) {} } }
[出来る事]ダイヤモンドオペレータ
これは例外ジェネリクスにも適用できます。
public class C<T, S, throws E> { public static void main(String[] args) { C<String, Integer, IOException> c = new C<>(); // OK } }
例外ジェネリクスの注意点
最後に、例外ジェネリクスの注意すべきことを書いて終わりたいと思います。
複数の例外ジェネリクスの型引数を使用してcatchする場合
public <throws A, throws B> void method(Callable<A | B> c) { try { c.call(); } catch (A a) {} catch (B b) {} }
一見何の問題もないように見えますが、実はこれはコンパイルエラーになります。
というのも、型イレイジャによってAとBはともにExceptionに置き換えられ、次のようになります。
public void method(Callable c) { try { c.call(); } catch (Exception a) {} catch (Exception b) {} }
つまり、Exceptionとして二回受け取ってしまっています。
これはちょっと残念な仕様かな。型イレイジャで実装される以上仕方がないけど。
[2012/03/28:修正:例外ジェネリクスの仮引数の使用可能な場所(4)について]
[2012/04/11:削除:例外ジェネリクスが導入されないことに伴って全体を削除]