[Java] Streamで例外処理を扱う

2017年4月17日月曜日

Java Stream API ジェネリクス

UncheckedIOException

Java8から UncheckedIOException という例外が追加されている。

https://docs.oracle.com/javase/jp/8/docs/api/java/io/UncheckedIOException.html

これは文字通り、RuntimeExceptionを継承した非検査例外だけど、なんでこんな例外が追加されたんだろうか。

例えば、Streamを使って、ファイルの内容をすべて標準出力に出力しようとすると、こんな感じになる。
package local;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

class FilesSample {
    public static void main(String ... args) {
        sample1();
    }

    private static void sample1() {
        Path path = Paths.get("./local/FilesSample.java");

        try (Stream<String> s = Files.lines(path)) {
     s.forEach(l -> System.out.println(l));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Files.linesは指定されたパスのテキストファイルを読み取って、行ごとにStreamで返してくれる。
ファイル読み込み処理なので、例外としてはIOExceptionが発生する。

UncheckedIOException発生!

じゃあ、今度はjavaファイルではなく、classファイルを読み込ませてみよう。
    private static void sample2() {
        Path path = Paths.get("./local/FilesSample.class");

        try (Stream<String> s = Files.lines(path)) {
     s.forEach(l -> System.out.println(l));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
$ java local.FilesSample
Exception in thread "main" java.io.UncheckedIOException: java.nio.charset.MalformedInputException: Input length = 1
 at java.io.BufferedReader$1.hasNext(BufferedReader.java:574)
 at java.util.Iterator.forEachRemaining(Iterator.java:115)
 at java.util.Spliterators$IteratorSpliterator.forEachRemaining(Spliterators.java:1801)
 at java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:580)
 at local.FilesSample.sample2(FilesSample.java:33)
 at local.FilesSample.main(FilesSample.java:15)
Caused by: java.nio.charset.MalformedInputException: Input length = 1
 at java.nio.charset.CoderResult.throwException(CoderResult.java:281)
 at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:339)
 at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:178)
 at java.io.InputStreamReader.read(InputStreamReader.java:184)
 at java.io.BufferedReader.fill(BufferedReader.java:161)
 at java.io.BufferedReader.readLine(BufferedReader.java:324)
 at java.io.BufferedReader.readLine(BufferedReader.java:389)
 at java.io.BufferedReader$1.hasNext(BufferedReader.java:571)
 ... 5 more

うーん、IOExceptionではなく UncheckedIOExceptionが発生してしまった...
しかも、発生位置はFiles.linesではなく、forEachでStreamを読み込んでいる箇所だ。

Files.linesがStreamを返していることから分かるように、すべての行を読み込んでからリストにして返しているのではなくて、ファイル内容を実際に読み込みにいくのはStreamが評価されるときになる。
なので、Files.linesでは例外が発生せずに、そのあとのStream.forEachで例外が発生したんだね。

でも、
Caused by: java.nio.charset.MalformedInputException
とあるように例外が発生した元々の原因はIOExceptionのサブクラスのMalformedInputExceptionだ。

Streamの評価で検査例外

じゃあ、Streamの評価中に検査例外が発生すると、どうなっちゃうんだろう。
試しにこんな無理矢理なコードを用意すると
    private static void sample3() {
        try (Stream<String> s = Stream.of("1", "2", "3")) {
     s.forEach(l -> { throw new IOException(); });
        }
    }
$ javac local/FilesSample.java 
local/FilesSample.java:38: エラー: 例外IOExceptionは報告されません。スローするには、捕捉または宣言する必要があります
            s.forEach(l -> { throw new IOException(); });
                              ^
エラー1個

と、見事にコンパイルエラーになってしまう。
これはStream.forEach(Consumer<? super T> action)の引数がConsumerになってて、その関数メソッドであるConsumer.acceptがthrows宣言をしていないからだね。

https://docs.oracle.com/javase/jp/8/docs/api/java/util/function/Consumer.html

そもそも、Consumer.acceptは実装がどんな例外を発生させるのか事前にに分からないし、Streamに検査例外をケアさせようとすると、すごく煩雑な記法になってしまいそうな気がする...

なので、結局、こんな感じで検査例外を非検査例外でラッピングすることになる。
    private static void sample4() {
        try (Stream<String> s = Stream.of("1", "2", "3")) {
            s.forEach(l -> {
                try {
                    throw new IOException();
                } catch (IOException e) {
                    throw new UncheckedIOException(e);
                }
            });
        }
    }
$ java local.FilesSample
Exception in thread "main" java.io.UncheckedIOException: java.io.IOException
 at local.FilesSample.lambda$sample4$2(FilesSample.java:49)
 at java.util.Spliterators$ArraySpliterator.forEachRemaining(Spliterators.java:948)
 at java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:580)
 at local.FilesSample.sample4(FilesSample.java:45)
 at local.FilesSample.main(FilesSample.java:14)
Caused by: java.io.IOException
 at local.FilesSample.lambda$sample4$2(FilesSample.java:47)
 ... 4 more

IOExceptionはいたるところに登場するので、Streamが導入されたJava8ではUncheckedIOExceptionも作られたんだね。

IOException処理のラッパー

さて、IO処理で毎回こんな例外処理を書くのもなんなので、IOExceptionをラッピングするConsumerを作っておくといいかもしれない。
package local;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.function.Consumer;
import java.util.Objects;

@FunctionalInterface
interface IOExceptionConsumer<T> {
    void accept(T t) throws IOException;

    static <T> Consumer<T> toUnchecked(IOExceptionConsumer<? super T> action) {
        Objects.requireNonNull(action);

        return t -> {
            try {
                action.accept(t);
            } catch (IOException e) {
                throw new UncheckedIOException(e);
            }
        };
    }
}

これで、先ほどの処理は以下のようになる。
    private static void sample5() {
        try (Stream<String> s = Stream.of("1", "2", "3")) {
            s.forEach(IOExceptionConsumer.toUnchecked(l -> {
                throw new IOException();
            }));
        }
    }