Javaの検査例外の存在意義

More than 3 years have passed since last update.

あらゆるところで否定され続けているJavaの検査例外だが、なぜこのような仕組みが必要なのか説明しようと思う。

この投稿は golang で複数のエラーをハンドリングする方法 という記事を見かけたので書いた。

いつも通り「コメントは大歓迎」です。


検査例外の話

検査例外の目的は 静的な分岐の強制 である。

例えばこのようなコードがあったとする。

f = File.open("dummy.txt")

f.write "Hello"
f.close

このようなAPIではプログラマが「ファイルを開けなかったときの処理を忘れる」ことを防ぐことができない。

一方、検査例外を使えば「開けなかったときの処理」をプログラマが忘れることができない。

忘れた場合はコンパイルエラーになり、そもそも実行できないからである。

(簡単のためRuby風なAPIにしている)

try {

File f = File.open("dummy.txt");
f.write("Hello");
f.close();
} catch (IOException e) { // catchしない場合コンパイルエラー
System.out.println("ファイルを開けませんでした。");
}


Maybe/Eitherの話

検査例外というのは、関数型言語におけるMaybeEitherの一種だと考えるのが良い。

型システムを利用して、失敗したときの処理を書き忘れるのを防ぐのが目的である。

Scala風なプログラムで、分岐を強制している例を挙げる。

val result = File.open("dummy.txt")

result match {
case Success(f) => f.write("Hello")
case Fail => System.out.println("ファイルを開けませんでした。")
}

このように分岐後でしか f に触れることができず、分岐を強制させることができる。


Kotlinの話

Kotlinでは「nullになる可能性がある変数」かそうでないかを明確に区別する。 参考

最近Facebookが発表したHackにも同様の機能があるようだ。

val file : File? = File.open("dummy.txt") // 型は明示的に書いているだけで省略可能

file.write("Hello") // コンパイルエラー

// OKな例
val file : File? = File.open("dummy.txt")
if (file != null) {
file.write("Hello") // ここのfileの型は<File?>ではなく<File>
} else {
print("ファイルを開けませんでした。");
}

通常の言語において、変数 file の型はずっと変わらないが、この言語ではifチェックの前後で型が異なるように振る舞う。


Goの話

(断わっておくが、Goのことはよく知らないので、間違いがあればコメントがほしい)

if file, err := file_open("dummy.txt"); err != nil {

print("ファイルを開けませんでした。");
} else {
file.write("Hello");
}

// 無視する例
file, _ := file_open("dummy.txt");
file.write("Hello");

Goは失敗時に二値を返すという方針のようだ。

値が二つある以上、「分岐を忘れる」ことは少なくなるが、強制はできないだろう。

また、正常系でも異常系でもfile, errに触れるのも問題である。


C#の話

これはC#でも同様である。C#では例外以外にはTryXxxを使うのが推奨されている。

File file;

if (File.TryOpen("dummy.txt", out file)) {
file.Write("Hello");
} else {
Console.WriteLine("ファイルを開けませんでした。");
}

// 無視する例
File file;
File.TryOpen("dummy.txt", out file);
file.Write("Hello");

関数は bool を返すため、分岐を忘れることは少なくなるが、強制できるわけではない。


まとめ

検査例外はそんなに悪い機能じゃない

コンパイルエラーを有効に使おう