信頼度ランク
| 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がすべての例外の頂点で、ExceptionとErrorに分かれる。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つの問題がある:
RuntimeException(バグ)まで握りつぶしてしまう- エラーの原因が分からなくなる
少なくともe.printStackTrace()かロギングをして、何が起きたかを記録すべきだ。またError(OutOfMemoryErrorなど)をcatchするのはほぼ常に誤りだ。JVMレベルの深刻な問題はアプリケーションコードでは回復できない。
まとめ
例外のメリット = 戻り値とエラー通知が混在しない。エラー処理を任意の場所でできる
スタックの遡り = throwされた例外はcatchされるまで呼び出し元を遡り続ける
checked例外 = コンパイラがcatch/throwsを強制。外部環境に依存するエラーに使う
RuntimeException = コンパイラが強制しない。プログラムのバグを表す。catchで握りつぶしはNG
finally = 例外の有無に関わらず実行される。リソースの後片付けに使う
try-with-resources = AutoCloseableなリソースを自動クローズ(Java 7以降)
checkedとuncheckedの設計判断はJava特有で議論も多い(現代のフレームワークはuncheckedを好む傾向がある)。ただ「コンパイラに強制させるほど重要なエラー」と「バグなので修正すべきエラー」という考え方自体は今も有効だ。