SJ blog
beginner
S

信頼度ランク

S 公式ソース確認済み
A 成功実績多数・失敗例少数
B 賛否両論
C 動作未確認・セキュリティリスク高
Z 個人所感

例外処理はなぜtry-catchなのか — エラー通知の設計思想とchecked/uncheckedの理由

なぜ戻り値でエラーを返さないのか。例外がスタックを遡る仕組み、checked例外とRuntimeExceptionの違いと存在理由、finallyとtry-with-resourcesを根本から解説。

一言結論

例外は「別チャンネルのエラー通知」でcatchされるまでスタックを遡る。checked例外はコンパイラが処理を強制し、unchecked(RuntimeException)はプログラムのバグを表す。両者の使い分けには明確な設計意図がある。

なぜ戻り値でエラーを返さないのか

C言語のような言語ではエラーをこう伝える:

int result = readFile("data.txt");
if (result == -1) {
    // エラー処理
}

Javaの例外と比べると何が違うのか。戻り値でエラーを返す方式には3つの問題がある。

問題1:戻り値の型が汚染される

本来の目的(ファイルの内容を返す)とエラー通知が混在する:

char* readFile(const char* path);
// 「char*を返す」のか「エラーコードを返す」のか、どうやって判別する?

問題2:呼び出し側がエラーチェックをしなくても通ってしまう

int data = readFile("data.txt");  // エラーでも-1が入るだけ
processData(data);  // -1のままprocessしてしまう(バグの温床)

エラーチェックを忘れてもコンパイラは何も言わない。

問題3:エラーチェックがネストして読みにくくなる

if (openFile() != -1) {
    if (readData() != -1) {
        if (parseData() != -1) {
            // ようやく本来の処理
        }
    }
}

本来の処理がエラーチェックのネストの奥深くに埋もれる。

例外はこれらの問題を「エラーを別のチャンネルで通知する」ことで解決する。

例外はスタックを遡る

例外がthrowされると、catchされるまでコールスタックを遡り続ける

void methodC() {
    throw new IOException("ファイルが見つからない");
}

void methodB() {
    methodC();  // ← 例外が飛んでくるが、catchしていない
}

void methodA() {
    try {
        methodB();
    } catch (IOException e) {
        System.out.println("エラー: " + e.getMessage());
        // ここでcatchされる
    }
}
methodC() でthrow
  ↓ スタックを遡る
methodB() (catchなし、そのまま上に伝わる)
  ↓ スタックを遡る
methodA() でcatch → ここで処理される

methodB()はエラー処理に関与しなくていい。methodC()methodA()だけが関係する。これが「本来の処理フロー」と「エラー処理フロー」を分離することを可能にしている。

どこにもcatchされなければ、最終的にスレッドが終了し、スタックトレースが表示されてプログラムが止まる。

Javaの例外ヒエラルキー

Throwable
├── Error          (JVMの深刻な問題: OutOfMemoryError など)
│                  → 通常はcatchしない
└── Exception
    ├── RuntimeException  (unchecked例外)
    │   ├── NullPointerException
    │   ├── ArrayIndexOutOfBoundsException
    │   ├── IllegalArgumentException
    │   └── ...
    └── IOException, SQLException, ...  (checked例外)

Throwableがすべての例外の頂点で、ExceptionErrorに分かれる。Exceptionの中にchecked例外と**unchecked例外(RuntimeException)**がある。

checked例外:コンパイラが処理を強制する

void readFile() throws IOException {  // 投げる可能性を宣言
    FileReader reader = new FileReader("data.txt");  // IOException を投げうる
}

// 呼び出し側は必ずどちらかをしないとコンパイルエラー
void main() {
    try {
        readFile();
    } catch (IOException e) {
        // 対処する
    }
    // または: void main() throws IOException としてさらに上に投げる
}

checked例外はコンパイラが「必ず対処せよ」と強制する。

なぜこういう仕組みがあるのか?

Javaの設計者たちは「ファイルが存在しない」「ネットワークが切れた」のような外部環境に依存するエラーは、プログラマーが必ず意識すべきだと考えた。コンパイラに強制させれば、対処を忘れることがなくなる。

「IOExceptionをthrowsに書いてtry-catchを強制されるのは面倒」と感じるかもしれないが、それが設計の意図だ。「このメソッドを呼ぶ以上、ファイルが見つからない可能性を考慮せよ」というシグナルだ。

unchecked例外(RuntimeException):プログラムのバグを表す

int[] arr = new int[3];
arr[10] = 1;  // ArrayIndexOutOfBoundsException → try-catchの強制なし

String s = null;
s.length();   // NullPointerException → try-catchの強制なし

RuntimeExceptionを継承した例外はコンパイラがtry-catchを強制しない。なぜか?

これらはプログラムのバグだからだ。配列の範囲外アクセスやnullのメソッド呼び出しは、設計・実装の誤りだ。「いつか起きるかもしれない例外」ではなく「起きてはいけない例外」だ。

catchして「なかったことにする」のは間違い。バグは修正すべきで、握りつぶすべきではない:

// ❌ 悪い例: バグを握りつぶしている
try {
    arr[10] = 1;
} catch (ArrayIndexOutOfBoundsException e) {
    // 何もしない → バグが隠れる
}

// ✅ 正しい対応: バグの原因を直す(インデックスが10になることがないようにロジックを修正)

自分でunchecked例外を投げる場合:

public void setAge(int age) {
    if (age < 0) {
        throw new IllegalArgumentException("年齢は0以上でなければなりません: " + age);
    }
    this.age = age;
}

「引数が不正」「状態が不正」といった「呼び出し側のプログラムミス」にはRuntimeException系を使う。呼び出し側にcatchを強制するより、バグを直してもらう方が正しいからだ。

finallyとはなぜあるのか

FileReader reader = null;
try {
    reader = new FileReader("data.txt");
    // ファイルを読む処理
} catch (IOException e) {
    System.out.println("読み込みエラー");
} finally {
    if (reader != null) {
        reader.close();  // 例外が起きても起きなくてもクローズする
    }
}

finallyは「例外が起きても起きなくても必ず実行される」ブロックだ。

ファイルやDBコネクションのような外部リソースは、使い終わったら必ず閉じなければならない。例外が起きたときにclose()を忘れると、リソースが解放されないまま残る(リソースリーク)。finallyはその「片付け処理」を保証するための仕組みだ。

Java 7からはtry-with-resourcesが使えて、AutoCloseableを実装したリソースは自動的にクローズされる:

// finallyを自分で書かなくていい
try (FileReader reader = new FileReader("data.txt")) {
    // ファイルを読む処理
} catch (IOException e) {
    System.out.println("読み込みエラー");
}
// try ブロックを抜けると自動的に reader.close() が呼ばれる

現代のJavaではtry-with-resourcesを積極的に使う。finallyで手動クローズするコードより明確でリークのリスクが少ない。

catchしてはいけないもの

// ❌ 避けるべきパターン
try {
    something();
} catch (Exception e) {  // 全例外を一括catchしている
    // 何もしない
}

2つの問題がある:

  1. RuntimeException(バグ)まで握りつぶしてしまう
  2. エラーの原因が分からなくなる

少なくともe.printStackTrace()かロギングをして、何が起きたかを記録すべきだ。またErrorOutOfMemoryErrorなど)をcatchするのはほぼ常に誤りだ。JVMレベルの深刻な問題はアプリケーションコードでは回復できない。

まとめ

例外のメリット      = 戻り値とエラー通知が混在しない。エラー処理を任意の場所でできる
スタックの遡り      = throwされた例外はcatchされるまで呼び出し元を遡り続ける
checked例外        = コンパイラがcatch/throwsを強制。外部環境に依存するエラーに使う
RuntimeException   = コンパイラが強制しない。プログラムのバグを表す。catchで握りつぶしはNG
finally            = 例外の有無に関わらず実行される。リソースの後片付けに使う
try-with-resources = AutoCloseableなリソースを自動クローズ(Java 7以降)

checkedとuncheckedの設計判断はJava特有で議論も多い(現代のフレームワークはuncheckedを好む傾向がある)。ただ「コンパイラに強制させるほど重要なエラー」と「バグなので修正すべきエラー」という考え方自体は今も有効だ。