はじめに:なぜ例外処理は実務で「必須」なのか?
はじめまして、学生エンジニアの@huyunokiです。
学校や自習でJavaを学んでいる時、例外処理(try-catch)は「エラーが出たときのおまじない」程度にしか考えていませんでした。しかし、インターンで実務コードに触れて痛感しました。
「例外処理を正しく書くこと」=「コードの堅牢性を保証すること」
コードが巨大になっても、トラブル時にシステム全体が壊れないように設計する能力こそがプロの証です。
この記事では、僕と同じように「例外処理がなんとなく分かっている」という方のために、Javaの例外クラスを整理し、実務で最も重要な検査例外と非検査例外の決定的な違いを解説します。
Part 1: Javaのトラブルの種類と例外の階層
Javaプログラムで発生するトラブルは、大きく2種類に分けられます。
1. Error(エラー)
- 原因: 実行環境の深刻なトラブルなど、プログラム側では対処しようのない事態
-
例: メモリ不足 (
OutOfMemoryError)、JVM内部の問題など - 対処: プログラム側でリカバリすることは難しく、通常はJVMの停止に繋がります
2. Exception(例外)
- 原因: 実行中に発生するトラブルのうち、プログラムが対処できる可能性のある事態
- 例: ファイルが見つからない、ネットワーク接続が切断された、など
-
対処:
try-catchブロックを使ってプログラムの実行を中断せずに回復処理(リトライ、ログ出力など)を記述できます
例外クラスの階層構造
Javaの例外クラスはすべて「java.lang.Throwable」クラスを頂点とする階層構造になっています。
この階層が、「検査例外」と「非検査例外」のルールを決定しています。
Part 2: 検査例外と非検査例外の決定的な違い
例外(Exception)は、「コンパイラがチェックするかどうか」によって以下の2種類に分類されます。
1. 検査例外 (Checked Exception)
| 特徴 | 内容 |
|---|---|
| コンパイラチェック | する |
| 発生するクラス |
Exceptionクラスの直下のサブクラス(RuntimeException系を除く) |
| コードのルール |
例外処理の記述が必須。try-catchするか、throws句で宣言しないとコンパイルエラーになる。 |
| 実務での用途 | 予期できるが、プログラムの外部で発生するトラブル(例:ファイルI/O、ネットワーク接続) |
なぜ存在するのか?
Javaが、プログラマーが例外処理を書き忘れることを強制的に防ぐためです。これにより、巨大なシステムでも「ファイルが見つからなかったらどうする?」という処理が抜け落ちることを防げます。
コード例(FileNotFoundException)
import java.io.FileReader;
import java.io.FileNotFoundException;
public class CheckedExceptionExample {
public static void main(String[] args) {
// FileReaderのコンストラクタは検査例外(FileNotFoundException)をスローする
try {
FileReader file = new FileReader("non_existent.txt");
// ファイル処理...
} catch (FileNotFoundException e) {
// try-catchがないとコンパイルエラーになる
System.out.println("エラー: ファイルが見つかりませんでした。");
}
}
}
2. 非検査例外 (Unchecked Exception)
| 特徴 | 内容 |
|---|---|
| コンパイラチェック | しない |
| 発生するクラス |
RuntimeException クラスとそのサブクラス |
| コードのルール | 例外処理の記述は任意。書かなくてもコンパイルエラーにはならない。 |
| 実務での用途 | 予期できないか、プログラマーのミスによって発生するトラブル(例:コードの論理的な欠陥) |
なぜチェックされないのか?
主に「コードの論理的なミス」であり、どこで発生するか予測が難しいためです。毎回try-catchを強制するとコードが煩雑になるため、通常はバグとして修正することが推奨されます。
コード例(NullPointerException)
public class UncheckedExceptionExample {
public static void main(String[] args) {
String data = null;
// nullの変数に対してメソッドを呼び出す
// try-catchがなくてもコンパイルは成功する
System.out.println(data.length());
}
}
// 実行結果:
// Exception in thread "main" java.lang.NullPointerException
Part 3: 実務で役立つ!例外の使い分けと対処法
1. 実務での判断基準(Throwablesの使い分け)
Javaの例外クラスは、以下の関係性で分類されます。
| クラス | 階層 | 例外タイプ | 対処法 |
|---|---|---|---|
Error |
Throwableの直下 |
- | システム停止(対処不能) |
Exception |
Throwableの直下 |
検査例外 |
try-catchで回復処理を記述 |
RuntimeException |
Exceptionのサブクラス |
非検査例外 |
バグとして修正し、try-catchは極力避ける |
2. 検査例外の対処法:throws句の利用
検査例外のもう一つの対処法であるthrows句は、例外処理を「このメソッドではなく、呼び出し元に任せる」という宣言です。
| 句 | 意味 | 使い分け |
|---|---|---|
try-catch |
この場所で例外を掴み、代替処理(回復)を行う。 | 実際にエラーから回復できる場合や、ログを必ず残したい場合。 |
throws |
このメソッドは責任を負わない。例外処理を呼び出し元メソッドに丸投げする。 | 最終的にController(呼び出しの最上位)でまとめて処理したい場合。 |
// throwsで例外処理の責任を呼び出し元に譲渡
public void readFile() throws FileNotFoundException {
FileReader file = new FileReader("data.txt");
// ファイル処理...
}
// 呼び出し元メソッドでtry-catch処理
public void process() {
try {
readFile();
} catch (FileNotFoundException e) {
// ここでエラーをキャッチし、ユーザーにメッセージを返すなどの処理を行う
System.out.println("処理失敗。ログを出力します。");
}
}
練習問題
今まででの内容から練習問題を作ってみたのでぜひチャレンジしてみてください。
【問題】
以下の2つの例外クラスをスローするメソッド targetMethod() があります。
-
MyCheckedException:
Exceptionを継承している(検査例外) -
MyUncheckedException:
RuntimeExceptionを継承している(非検査例外)
この targetMethod() がコンパイルエラーにならずに実行されるために、関数のシグネチャの throws句にとなるのはどのパターンですか。(正解は2つ)
// 仮定:このメソッド内で、両方の例外が throw new されている
public void targetMethod(String str) /* throws 〇〇〇 */ {
if(str == null){
throw new MyCheckedException();
}else{
throw new MyUncheckedException();
}
}
| 選択肢 | 記述内容 |
|---|---|
| 1 |
throws MyCheckedException, MyUncheckedException の両方を記述する。 |
| 2 |
throws MyCheckedException のみ記述する。 |
| 3 |
throws MyUncheckedException のみ記述する。 |
| 4 | 何も記述しない (throws句自体を省略する)。 |
こちらクリックすると正解が出てきます。まずは考えてみましょう!
【正解】
-
throws MyCheckedException, MyUncheckedExceptionの両方を記述する。 -
throws MyCheckedExceptionのみ記述する。
【解説】
この問題のポイントは、「検査例外の記述は必須だが、非検査例外の記述は任意である」という点です。
-
検査例外 (MyCheckedException):
Exceptionを継承しているため、コンパイラはthrows句での宣言を強制します。これを書かないとコンパイルエラーになります。 -
非検査例外 (MyUncheckedException):
RuntimeExceptionを継承しているため、コンパイラによるチェックはスキップされます。throws句に記述しても問題ありませんが、記述しなくてもコンパイルエラーにはなりません。
したがって、必須の検査例外(MyCheckedException)が含まれており、かつコンパイルエラーにならないパターンである「1」と「2」が正解となります。
- 正解1:必須の検査例外と、任意の非検査例外の両方を宣言。(記述可能)
- 正解2:必須の検査例外のみを宣言。(記述が必須のものである)
- 不正解3, 4:検査例外の宣言がないため、コンパイルエラーとなります。
まとめ:例外は「バグ」と「仕様」を見分ける力
例外を正しく理解することは、「これは自分のコードのミス(非検査例外)なのか、外部環境のトラブル(検査例外)なのか」を見分ける力、すなわちデバッグ能力と設計能力に直結します。
-
非検査例外 (
RuntimeException系) → 直ちにコードを修正すべき「バグ」。 -
検査例外 (
Exception系) → 発生を前提とした代替処理を「仕様」として記述。
最後に:読者の皆さまへ
最後までお読みいただき、本当にありがとうございました!
この知識が、皆さんのエンジニア人生の支えになれば幸いです。
僕はまだ学習中の身ですので、もし内容に誤りやより良い実践方法がございましたら、遠慮なくコメントでご指摘ください!皆さんからのコメント、心からお待ちしてます!
参考本
以下の本を参考にしましたので興味があればぜひ!
