クロージャにおける例外のスロー

3回にわたってJava SE 7で導入される予定のクロージャについて紹介してきたが、少しずつ具体的な形が見えてきただろうか。今回はJavaのクロージャにおける例外の扱いについて紹介したい。

クロージャでは例外をスローすることもできる。そのためにはリスト1のように、throwsキーワードを使ってスローしたい例外クラスを戻り値の後に指定すればよい。

リスト1 ExceptionSample.java - 例外の指定

import java.io.*;

class ExceptionSample {
    public static void main(String[] args) {
        try {
            Writer writer = 
                new PrintWriter(new FileWriter("sample.txt"));

            // 例外付きのクロージャ
            { String => void throws IOException } write = 
                { String message => writer.append(message + "\n"); };

            try {
                write.invoke("Good morning!");
                write.invoke("Good afternoon!");
            } finally {
                writer.close();
            }
               } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

このクロージャではWriterのappend()メソッドを呼び出しているが、append()はIOExceptionをスローする可能性がある。そこでthrowsでIOExceptionを指定することによって、通常のメソッドの場合と同様に例外がスローされることを明示している。

クロージャのブロックステートメント内で例外がスローされる可能性がある場合、そのクロージャの関数型は自動的にthrowsで修飾されたものになり、throwsの付かない関数型との互換性はなくなる。たとえば前述の例の場合、リスト2のようにthrowsの指定を外してしまうと、プロンプト1に示すようなコンパイルエラーになる。

リスト2 throws指定のないクロージャ

{ String => void } write = 
    { String message => writer.append(message + "\n"); };

プロンプト1 throwsで修飾されたクロージャとそうでないクロージャは互換性がない

ExceptionSample.java:13: 互換性のない型
検出値  : {java.lang.String => void throws java.io.IOException}
期待値  : {java.lang.String => void}

では、例外をスローするクロージャの実装はどうなっているのか見てみよう。まず、このクロージャの型を表すインタフェースjavax/lang/function/unrestricted/VO.classを逆コンパイルしてみるとリスト3のようになっており、invoke()メソッドにthrowsが指定されていること分かる。このinvoke()の実装はExceptionSample.classを逆コンパイルすれば見られるが、要約するとリスト4のような感じだ。したがってinvoke()呼び出し時にはIOExceptionをcatchしなければならない。

リスト3 VO.classを逆コンパイル

public interface VO {
    public abstract void invoke(Object obj) throws Throwable;
}

リスト4 ExceptionSample.classを逆コンパイル

public final void invoke(String s) throws IOException {
    writer.append((new StringBuilder())
            .append(s).append("\n").toString());
}

型引数の利用

Java SE 7ではクロージャの追加に合わせて、ジェネリクスの型引数で例外のthrow宣言ができるようになるという。これを例外型引数(Exception Type Parameters)と呼び、これを利用することでスローする例外の種類を実行時に決定できるようになる。例外型引数はたとえばリスト5のように記述する。

リスト5 ExceptionSample2.java - 例外型引数の使用例

import java.io.*;

class ExceptionSample2 {
    static <throws E> void todo({ => void throws E } block) throws E {
        block.invoke();
    }

    public static void main(String[] args) {
        // 例外なしでの呼び出し
        todo({ => System.out.println("Hello!"); });

        try {
            Writer writer = 
                new PrintWriter(new FileWriter("sample.txt"));
            // 例外付きの呼び出し
            todo({ => writer.append("Hello!"); });
           } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

この例の場合、todo()メソッドには引数として何らかの例外をスローする可能性のあるクロージャを渡す。その例外の種類は例外型引数で指定されているので実行時に決まる。最初の呼び出しでは単に"Hello!"と表示するだけのクロージャなので、例外はスローされない。2番目の呼び出しではWriterのappend()を使うクロージャなので、IOExceptionがスローされる可能性がある。したがって呼出し時にはこれをcatchする必要がある。

なお、もしクロージャの実行時にスローする例外を明示的に指定したい場合にはリスト6のようにすればいいとのこと。

リスト6 例外を明示的に指定

ExceptionSample2.<throws IOException>todo({ => writer.append("Hello!"); });

複数の例外をスローする

クロージャで複数の例外をスローするように宣言することも可能。それには、通常のthrows宣言と同様にスローしたい例外のクラスをコンマで区切って並べればよい。リスト7はIOExceptionとInterruptedExceptionを同時に指定した例である。これは渡されたた文字列をファイルに書き込むクロージャだが、書き込み後にThread.sleep()で100msスリープさせているため、InterruptedExceptionをスローする可能性がある。したがってInterruptedExceptionのthrows宣言を省略するとコンパイルエラーになる。

なお、catchブロックの「catch (IOException | InterruptedException e)」というのは、Java SE 7で導入される予定のマルチキャッチの機能を用いた記述。キャッチしたい例外を"|"で続けて記述することにより、ひとつのcatchブロックで複数の例外を処理することができるようになる。

リスト7 ExceptionSample3.java - 複数の例外をスローする例

import java.io.*;

class ExceptionSample3 {
    public static void main(String[] args) {
        // 複数の例外をスローする
        { String => 
                void throws IOException, InterruptedException } write = 
            { String message => 
                 Writer writer = 
                     new PrintWriter(new FileWriter("sample.txt"));
                 writer.append(message + "\n");
                 Thread.sleep(100);
                 writer.close();
            };

            try {
                write.invoke("Good morning!");
                write.invoke("Good afternoon!");

            } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
    }
}

例外型引数を使う場合にも複数の例外をスローするように明示的に指定することができる。この場合、のときのように呼出し側で具体的な例外のクラスを指定するが、その際にマルチキャッチのときと同様に"|"で区切って任意の例外を並べて記述する。たとえば先述のtodo()メソッドの呼び出しならばリスト8のようになる。

リスト8 例外型引数を使う場合に、複数の例外を明示的に指定

try {
    // 複数の例外を明示的にスロー
    ExceptionSample2_3.
            <throws IOException | InterruptedException>todo(
        { => Writer writer = 
                 new PrintWriter(new FileWriter("sample.txt"));
             writer.append("Hello!");
             Thread.sleep(100);
             writer.close();
        });
} catch (IOException | InterruptedException e) {
    e.printStackTrace();
}

この例でtodo()に渡しているのは、リスト7と同様にIOExceptionとInterruptedExceptionの両方をスローする可能性があるクロージャである。したがってこの2つのクラスをthrowsで明示的に宣言している。