Java | Java 標準ライブラリ:チェック例外

Java Java
スポンサーリンク

チェック例外を一言でいうと

「チェック例外(checked exception)」は、

“この失敗は普通に起こりうるから、呼び出し側でちゃんと対処を考えろ” とコンパイラが迫ってくる例外

です。

ファイルがない、ネットワークが切れた、SQL が失敗した、など
外部要因で簡単に起こりうる失敗を、「見て見ぬふりさせない」ための仕組みです。

コンパイラが

その例外を throws に書くか
try-catch で捕まえるか

のどちらかを絶対に要求してくる、というのが一番の特徴です。


チェック例外はどこに位置しているか(階層構造の中で)

Exception の中の「RuntimeException 以外」

例外階層をざっくり書くと、こうなります。

Throwable
Error
Exception

このうち、アプリが主に扱うのは Exception の世界です。
そのさらに中で、

RuntimeException(とそのサブクラス)
それ以外の Exception

に分かれます。

チェック例外というのは、この「RuntimeException 以外の Exception」のことです。

つまり、型としてはこうイメージしてください。

Exception のうち
RuntimeException とその子孫 → 非チェック例外(unchecked)
それ以外 → チェック例外(checked)

コンパイラに「対応を強制されるかどうか」の境界が RuntimeException です。


代表的なチェック例外と「なぜチェックされるのか」

IOException 系

ファイル・ネットワークなどの I/O で出る例外です。

ファイルが存在しない
アクセス権限がない
読み込み中にエラーが起きた

など、外部環境によって普通に起こる失敗を表します。

例として、Files.readString の宣言を考えます(実際はオーバーロードがありますが、イメージとして)。

String readString(Path path) throws IOException

この throws IOException のせいで、呼び出し側は必ず

try-catch で捕まえる
あるいは、さらに自分も throws IOException を書く

のどちらかをしないとコンパイルが通りません。

これは「ファイル読み込みは失敗しうるのが普通だから、その可能性を無視するな」というメッセージです。

SQLException

データベース操作での失敗に対して投げられます。

SQL の文法ミスや
テーブルが存在しない
接続が切れた

など、やはり外部要因によって普通に起こりうる失敗です。

JDBC のメソッド群はほとんど throws SQLException になっているので、
DB を触るコードを書くときは、必ず例外処理を考えさせられます。

その他のチェック例外

日時のパース失敗を表す ParseException など、
「入力が正しいとは限らない」ものを扱う API ではチェック例外が使われることが多いです。

共通しているのは、

真面目に生きていても普通に起こる失敗
プログラマーの単純なバグとは限らない

という性質です。


コンパイラから見た「チェック例外のルール」

throws する側のルール

メソッドがチェック例外を投げる可能性があるとき、そのメソッドは宣言に throws を書かないといけません。

例:

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

public class CheckedThrower {
    public static void read() throws IOException {
        String s = Files.readString(Path.of("data.txt")); // ここで IOException の可能性
        System.out.println(s);
    }
}
Java

この read() メソッドは、「自分の中で IOException を捕まえずに外へ投げる」ので、
throws IOException が必須です。

もし throws を書かないと、コンパイルエラーになります。

つまり、

チェック例外を投げうるコードを書いたなら
そのメソッドは「自分で catch する」か「throws で宣言する」かを決めろ

とコンパイラが迫ってくるわけです。

呼び出し側のルール

さっきの CheckedThrower.read() を呼ぶ側も、やはり対応を迫られます。

例:

public class Caller {
    public static void main(String[] args) {
        try {
            CheckedThrower.read();
        } catch (IOException e) {   // 捕まえる
            e.printStackTrace();
        }
    }
}
Java

あるいは、main 自体がさらに throws しても構いません。

public static void main(String[] args) throws IOException {
    CheckedThrower.read();   // ここでは try-catch しない
}
Java

いずれにせよ、

チェック例外は「どこかの時点で必ず宣言か捕獲が必要」
宣言も捕獲もしなければコンパイルエラー

というのが、コンパイラから見たルールです。


実行時例外との違いを、コードで肌感として掴む

実行時例外(RuntimeException)はコンパイラがノータッチ

例えば、こんなメソッドを考えます。

public class RuntimeThrower {
    public static void throwRuntime() {
        throw new IllegalArgumentException("不正な引数です");
    }
}
Java

IllegalArgumentExceptionRuntimeException のサブクラスなので、
このメソッドに throws を書く必要はありません。

呼び出し側も、try-catch を強制されません。

public class RuntimeCaller {
    public static void main(String[] args) {
        RuntimeThrower.throwRuntime();  // コンパイルは通る
        System.out.println("ここには到達しない");
    }
}
Java

実行すると例外で落ちますが、それは「実行時の話」です。
コンパイラは何も言ってきません。

ここがチェック例外との決定的な違いです。

チェック例外
→ コンパイル時に「ちゃんと扱え」と言われる

実行時例外
→ コンパイル時には何も言われない。落ちるかどうかは実行してみないと分からない

設計の観点から言うと、
「どこかの層でこの例外に向き合うべきかどうか」を、
チェック例外かどうかで表現している、とも言えます。


チェック例外をどう扱うか(try-catch と throws の設計)

その場で回復するなら try-catch

ファイルがなければ「空の設定」として扱う
パースに失敗したら「デフォルト値」にフォールバックする

など、その場で「どうにかできる」なら、try-catch するのが自然です。

import java.io.IOException;
import java.nio.file.*;

public class ConfigLoader {
    public static String loadConfig() {
        Path path = Path.of("config.txt");
        try {
            return Files.readString(path);
        } catch (IOException e) {
            System.err.println("設定ファイル読み込み失敗。デフォルト設定を使います: " + e.getMessage());
            return "default-config";
        }
    }
}
Java

ここでは「外側に例外を投げ返さない」選択をしていて、
呼び出し側から見れば「常に String を返してくれるメソッド」です。

呼び出し側に判断を任せたいなら throws

一方で、「この層ではどうにもしようがない」ときもあります。

例えば、ライブラリやフレームワークの内部で、
上にいるアプリケーション層に「失敗したことを知ってほしい」だけの場合などです。

import java.io.IOException;
import java.nio.file.*;

public class RawConfigLoader {
    public static String loadConfig(Path path) throws IOException {
        return Files.readString(path);
    }
}
Java

この場合、呼び出し側は必ず try-catch か throws を書かないとコンパイルが通りません。
どこで握りつぶすか、どこで再スローするか、を設計として考える必要があります。

設計上の考え方としては、

この層で「意味のある回復」ができるか
できないなら、「より上位の層」に判断を委ねるべきでは?

という観点を持つと、throws か try-catch かの判断がしやすくなります。


チェック例外を RuntimeException にラップする、という選択

どうしてもチェック例外が邪魔に感じることがある

例えば、Stream API の中や、ラムダ式の中では、
チェック例外をそのまま投げられない場面があります。

Files.lines(path).map(line -> {
    // ここで checked exception を投げにくい
});
Java

また、「アプリ全体としては、ここでの I/O 失敗を回復する術はない」と判断し、
わざわざ全呼び出し元で try-catch したくないケースもあります。

そんなときによく使われるテクニックが、

チェック例外をキャッチして、自作の RuntimeException に包んで投げ直す

という方法です。

ラップの具体例

public class UncheckedIoException extends RuntimeException {
    public UncheckedIoException(IOException cause) {
        super(cause);
    }
}
Java

これを使って、例えば設定読み込みをこう書き換えます。

import java.io.IOException;
import java.nio.file.*;

public class ConfigLoader2 {
    public static String loadConfig(Path path) {
        try {
            return Files.readString(path);
        } catch (IOException e) {
            throw new UncheckedIoException(e); // RuntimeException に包んで再スロー
        }
    }
}
Java

呼び出し側は、コンパイル時には何も強制されませんが、
実行時に問題があれば UncheckedIoException として表面化します。

これは、

この I/O の失敗は、この層では回復不能
設計上、「エラーとして落ちて構わない」とみなしている

という意思表示でもあります。

やみくもに全部ラップするのはお勧めしませんが、
アプリ全体のエラー方針として「ここは unchecked でいい」と判断した場所では有効な手法です。


まとめ:チェック例外を自分の中でこう位置づける

チェック例外を初心者向けにまとめると、こうなります。

  • Exception のうち、RuntimeException 以外のもの
  • コンパイラが「throws か try-catch を必ず書け」と強制してくる例外
  • 外部要因や入力の不正など、“ちゃんと設計に組み込むべき失敗” を表すことが多い

設計・実装の観点では、

  • この例外は「その場で回復する」のか
  • 「上位に投げて判断させる」のか
  • それとも「致命的とみなして RuntimeException にラップする」のか

を意識して選ぶのが大事です。

タイトルとURLをコピーしました