throws 節に型変数を使って任意の例外を投げる forEach メソッドが書ける

id:miyakawa_taku:20140609:1402320680 の続きです。

Java SE 8 では Iterable インタフェースに forEach メソッドが用意されました。いわゆる内部イテレータで、通常は次のようにラムダ式をともなって使います。

List<String> list = ...;
list.forEach(str -> System.out.println(str));

forEach に渡すラムダ式の本体では、チェック例外を投げることができません。 forEach の引数は Consumer インタフェースで、その Consumer.accept メソッドには throws 節がないからです。

したがって次のコードはコンパイルエラーになります。 Writer.write メソッドはチェック例外である IOException を投げるからです。

try (Writer w = new FileWriter("hoge.txt")) {
    list.forEach(str -> w.write(str));  // ← だめ
}

ここで、 throws 節の型変数を使うと、任意の型の例外を投げる内部イテレータが実現できます。具体的には、例外の型を型パラメータとして取る関数型インタフェースと、同じく例外の型を型パラメータとして取る forEach 相当メソッドを定義します。

import java.util.*;

@FunctionalInterface
interface ThrowingConsumer<T, E extends Throwable> {
    void accept(T t) throws E;
}

public class Main {
    static <T, E extends Throwable> void
    forEachThrowing(
            Iterable<T> iter
            , ThrowingConsumer<T, E> cons)
            throws E {
        for (T element : iter) {
            cons.accept(element);
        }
    }

    public static void main(String[] args) throws IOException {
        List<String> list = Arrays.asList(args);
        try (FileWriter w = new FileWriter("hoge.txt")) {
            forEachThrowing(list, str -> w.write(str));
        }
    }
}

今回の例では拡張 for 文使えばいいじゃん、ってだけの話なのですが、ユーザのコードが発生した例外をライブラリが catch せずに、そのままユーザ側に伝播したい、というケースでは使い回せる手です。たとえば、ツリー構造を Visitor パターンで走査する場合の例について、 id:backpaper0 さんの記事がありました。

以上述べた方式には難点があって、処理中で複数の型のチェック例外が投げられる可能性がある場合、例外型の個数分だけ型パラメータを用意しなきゃなりません。こうなると、ラムダ式型推論がうまく行かなくて、明示的にキャストしてやる必要があり *1 、次のようにあまり芳しくないコードになってしまいます。

import java.util.*;

@FunctionalInterface
interface Throwing2Consumer<T, E1 extends Throwable, E2 extends Throwable> {
    void accept(T t) throws E1, E2;
}

public class Main {
    static
    <T, E1 extends Throwable, E2 extends Throwable> void
    forEachThrowing2(
            Iterable<T> iter
            , Throwing2Consumer<T, E1, E2> cons)
            throws E1, E2 {
        for (T element : iter) {
            cons.accept(element);
        }
    }

    public static void main(String[] args)
            throws IOException, ClassNotFoundException {
        List<String> list = Arrays.asList(args);
        try (FileWriter w = new FileWriter("hoge.txt")) {
            forEachThrowing2(list
            , (Throwing2Consumer<String, IOException, ClassNotFoundException>)
            (str -> {
                Class.forName("no.such.Class");
                w.write(str);
            }));
        }
    }
}

*1:型推論の規則がよく分かっていないので、コンパイラの挙動を元に書いています