Javaの例外処理は、try-catch-finallyで例外を捕捉し、throw・throwsでスロー元と処理責任を分担し、必要に応じてカスタム例外でエラーの意味を明確にするという3つの基本を組み合わせて設計します。
つまずきやすいのはtry-catchの書き方そのものより「いつ・どの例外を・どう処理すべきか」の判断であり、この記事ではその判断軸を実務でそのまま使えるサンプルコード付きで解説します。
10年以上のJava開発経験で数多くのNullPointerExceptionやIOExceptionと格闘してきた私が、現場で本当に役立つ例外処理の知識を初心者にもわかりやすくまとめました。
あわせて、コンパイラが処理を強制する検査例外(チェック例外)と、強制しない非検査例外の違い、Spring Bootやラムダ式が絡む2026年の実務での扱い方も解説します。
読み終える頃には、catchブロックを空にする、Exceptionで安易に捕捉するといった「やってはいけない」例外処理を避けられるようになります。
この記事でわかること
- Javaの例外処理の基本的な考え方と、検査例外(Checked)・非検査例外(Unchecked)の違い
- try-catch-finally文の正しい書き方と使い分け
- throwとthrowsの違いと使いどころ
- カスタム例外の作り方と、検査例外・非検査例外の使い分け
- Spring Bootやラムダ式・ストリームAPIでの例外設計の勘所
- 例外処理のベストプラクティス(ログ出力・回復処理・戦略設計)
この記事は、Java入門ロードマップの「STEP5|実務で使う基礎」に対応しています。全体の学習順は次の記事で確認できます。
PARTS LIST /目次
- Java例外処理の基本:例外とは何か
- try-catch-finallyの書き方【Java例外処理】
- Javaの例外をスローする:throwとthrowsの違い
- Javaカスタム例外の作り方と実践例
- 検査例外と非検査例外の使い分け
- 検査例外を避けるべきケース|ラムダ式・ストリームAPI
- Java例外処理のベストプラクティス
- よくある質問
- Q1. Javaの例外処理はなぜ必要ですか?
- Q2. 検査例外(Checked Exception)と非検査例外(Unchecked Exception)の違いは何ですか?
- Q3. try-catchとtry-catch-finallyの違いは何ですか?
- Q4. throwとthrowsの違いは何ですか?
- Q5. カスタム例外はいつ作るべきですか?
- Q6. catchブロックは空にしてもいいですか?
- Q7. 検査例外は時代遅れですか?
- Q8. throws Exception と書くのはアリですか?
- Q9. try-with-resourcesを使えば検査例外の処理は不要になりますか?
- Q10. ラムダ式の中で検査例外をそのままスローできないのはなぜですか?
- Q11. Java資格試験では検査例外はどう問われますか?
- まとめ:Javaの例外処理を使いこなそう
Java例外処理の基本:例外とは何か
Javaでは、エラーの発生を避けて通れません。
Javaの例外処理は、エラーという予期せぬ事態に対応し、プログラムが突然停止するのを防ぐための重要な仕組みです。
例外とは?なぜ必要なのか?
例外とは、プログラムの実行中に発生する「予期せぬできごと」や「エラー」を指します。例えば、存在しないファイルを開こうとしたり、整数をゼロで割り算しようとしたりする場合に発生します。
もし例外処理がなければ、プログラムはエラーが発生した時点で強制的に終了してしまいます。
強制終了すると、ユーザーが入力していたデータが失われたり、システム全体が不安定になります。
例外処理を適切に行うことで、以下のことが可能になります。
- プログラムの強制終了を防ぐ: エラーが発生しても、それを検知して代替処理を行い、プログラムの実行を継続させます。
- エラーの原因を特定する: 発生した例外の種類やメッセージから、どこで何が問題だったのかを特定しやすくなります。
- 安全な状態に復旧する: ファイルを閉じる、データベースの接続を元に戻すなど、エラー発生後もリソースを安全に解放できます。
システムの安定性と信頼性を高める上で、例外処理は欠かせない技術です。
例外の種類:Checked Exception(検査例外)とUnchecked Exception(非検査例外)
Javaの例外は、大きく分けて2つの種類があります。検査例外(Checked Exception、チェック例外)と、非検査例外(Unchecked Exception)です。
両者を分ける最も大きな違いは、コンパイラが処理を強制するかどうかです。両方の特徴を理解することが、適切な例外処理を行う第一歩です。
検査例外
- コンパイル時にチェックされ、プログラマに例外処理を強制します
Exceptionクラス自身と、そのサブクラス(RuntimeException系を除く)がこれにあたります- 回復可能性のある、予期すべきエラー通知に使われます
非検査例外
- コンパイル時にチェックされず、処理を強制されません
RuntimeExceptionとErrorの2種類がありますRuntimeException: プログラムのバグなど、主にプログラマのミスが原因ですError: システムの致命的な問題で、プログラム側では回復不可能な状況を示します
実はCheckedException(チェック例外)という仕組みはJava特有の設計で、C#やKotlinなど後発の言語ではあえて採用されていません。
呼び出し元に対応を強制する構文が冗長になりやすいという批判があるためです。
実務では、「呼び出し元に対処を強制したいほど重要なエラーだけをCheckedにする」という基準で運用するのが現実的です。
何でもCheckedにするとthrows句がメソッドシグネチャを埋め尽くし、かえって可読性が落ちるためです。
Checked Exception(検査例外)の定義と特徴
コンパイル時にJavaコンパイラがチェックする例外です。
プログラマーには、次のどちらかの対処が強制されます。どちらも行わないと、コンパイルエラーになってプログラムを実行できません。
ポイント
try-catchブロックで例外を捕捉し、その場で処理するthrows宣言を使い、メソッドの呼び出し元に例外処理の責任を転嫁する
主に、プログラムの外部要因(ネットワーク、ファイルシステムなど)によって発生しうる、回復可能なエラーが該当します。
- 代表的な例:
IOException,SQLException,ClassNotFoundException
例えば、ファイルの読み込み処理を書く場合、そのファイルが存在しない可能性は常にあります。
そのため、Javaは IOException に対する処理をコンパイルの段階で要求するのです。
Javaがこの強制的な仕組みを導入したのは、ファイル操作やネットワーク通信のように外部環境に依存する処理は成功が保証されないからです。「この処理は失敗する可能性があるから備えておきなさい」と開発者に意識させ、予期せぬ停止を防ぐのが狙いです。
Unchecked Exception(非検査例外・実行時例外)の定義と特徴
コンパイル時にはチェックされず、プログラムの実行時に発生する例外です。
RuntimeExceptionクラスとそのサブクラスがUnchecked Exceptionに該当します。
主に、プログラマーのコーディングミスや設計上の問題といった、プログラム内部の論理的な誤りが原因で発生します。
- 代表的な例:
NullPointerException,ArrayIndexOutOfBoundsException,IllegalArgumentException
nullのオブジェクトのメソッドを呼ぶとNullPointerExceptionが発生します。
Unchecked Exceptionは例外処理で対応するのではなく、コードを修正して防ぐべき問題です。そのためコンパイラは処理を強制しません。
新人のうちは、NullPointerExceptionを何度も出しがちです。Optionalの使い方を覚え、nullチェックのif文の代わりに「値がないかもしれない」ことを型で表現する習慣がつくと、NPEを未然に防げるようになります。
なお、非検査例外にはもう1つErrorがあります。OutOfMemoryErrorのようなJVMレベルの致命的な問題を示し、プログラム側では回復できません。
Throwableの継承関係で検査例外を見分ける
検査例外かどうかは、クラスの継承関係で機械的に判定できます。すべての例外の頂点にいるのがThrowableで、その下にExceptionとErrorが並びます。
Throwable
├── Exception … 検査例外の親(RuntimeExceptionを除く)
│ ├── IOException
│ ├── SQLException
│ └── RuntimeException … ここから下は非検査例外
│ ├── NullPointerException
│ └── IllegalArgumentException
└── Error … 非検査例外(JVMレベルの致命的問題)
├── OutOfMemoryError
└── StackOverflowError
つまり、Exception自身と、ExceptionのサブクラスのうちRuntimeExceptionのサブクラスではないものが、検査例外です。
迷ったら、Javadocでそのクラスの継承チェーンを1段ずつ遡り、RuntimeExceptionにたどり着くかどうかを確認します。
よく使う例外クラス一覧
ここまでで登場したものを含め、Javaでよく遭遇する代表的な例外クラスを一覧にまとめました。
まずはこの一覧を眺めて「どの例外がChecked/Uncheckedのどちらに属するか」の感覚をつかんでおくと、以降の解説が理解しやすくなります。
代表的な検査例外|IOException・SQLException・InterruptedException
検査例外の中でも、実務で特によく出会うものの使いどころを整理します。
- IOException:データの入出力(I/O)で問題が起きたときにスローされます。ファイルの読み書き(
FileReader・FileWriter)、ネットワーク通信(Socket・URL)、外部プロセスとのやりとりで登場します。存在しないファイルの読み込みや、書き込み先の容量不足が典型例です。 - SQLException:JDBCでデータベースを操作するときに発生します。接続失敗、SQL構文エラー、一意制約違反などを通知します。トランザクション管理と組み合わせ、データの一貫性を保つ役割を担います。
- ClassNotFoundException:クラスパス上に対象のクラスが見つからない場合に発生します。リフレクションなどでよく見られます。
- InterruptedException:待機・スリープ中のスレッドに、別のスレッドから割り込みがかかったときにスローされます。Java 21の仮想スレッドでも扱いは変わらず、割り込みを受けたら握りつぶさずに再スローするか、割り込みフラグを立て直す(
Thread.currentThread().interrupt())のが定石です。
try-catch文の基本構文
例外処理の最も基本的な構文が try-catch 文です。
例外が発生する可能性のある処理を try ブロックで囲み、発生した例外を catch ブロックで捕捉して対応します。
try {
// 例外が発生する可能性のある処理
int[] numbers = {1, 2, 3};
System.out.println(numbers[5]); // ArrayIndexOutOfBoundsExceptionが発生
} catch (ArrayIndexOutOfBoundsException e) {
// 例外を捕捉したときの処理
System.out.println("配列の範囲外にアクセスしました。");
System.out.println("エラー詳細: " + e.getMessage());
}
System.out.println("プログラムは正常に処理を続けました。");
上の例では、配列の5番目(存在しない)にアクセスしてArrayIndexOutOfBoundsExceptionが発生します。
catchブロックで捕捉しているため、プログラムは強制終了しません。エラーメッセージを出力した後、後続の処理が実行されます。
try-catch-finallyの書き方【Java例外処理】
try-catch-finallyは、try(処理の実行)・catch(例外の捕捉)・finally(後処理)の3ブロックを組み合わせ、より柔軟で安全な例外処理を実装する構文です。
tryブロック:例外が発生する可能性のあるコード
try ブロックには、例外を発生させる可能性のあるコードを記述します。
ファイル操作、ネットワーク通信、データベース接続など、外部リソースを扱う処理が典型例です。
try {
// データベースへの接続や、ファイルの読み込み処理など
// この中で例外が発生すると、即座に実行が中断される
int result = 100 / 0; // ArithmeticExceptionが発生
System.out.println("この行は実行されません。");
} catch (ArithmeticException e) {
// ... catchブロックの処理 ...
}
try ブロック内で例外が発生した瞬間、それ以降の処理は実行されません。
Javaの実行環境は、発生した例外に対応する catch ブロックを探し始めます。
catchブロック:例外を捕捉して処理する
catch ブロックは、try ブロックで発生した例外を捕捉し、その後の対応を記述する場所です。
catch の後の括弧 () には、捕捉したい例外のクラスを指定します。
複数の種類の例外に対応するために、catch ブロックを複数記述することも可能です。
その際は、より具体的な例外クラス(サブクラス)から順に記述する必要があります。
try {
// ... 例外が発生する可能性のある処理 ...
} catch (FileNotFoundException e) {
System.out.println("指定されたファイルが見つかりません。");
} catch (IOException e) {
System.out.println("ファイルの入出力エラーが発生しました。");
} catch (Exception e) {
System.out.println("予期せぬエラーが発生しました。");
e.printStackTrace(); // 開発時にエラーの詳細を確認するのに便利
}
catch ブロックの引数で受け取った例外オブジェクト (e) から、エラーの詳細情報を取得できます。
getMessage() でエラーメッセージを、printStackTrace() でスタックトレース(メソッドの呼び出し履歴)を確認でき、デバッグに役立ちます。
catch ブロックを空にすることは避けてください。 エラーが検知されずに見過ごされ、後の工程でより深刻な問題を引き起こす原因となります。
たとえば、空のcatchブロックがNullPointerExceptionを握りつぶし、画面にエラーを表示しないまま不正なデータを保存してしまう、といった障害は珍しくありません。原因調査に半日以上かかることもあります。
空の catch ブロックは、未来の自分やチームメンバーへの時限爆弾です。
マルチキャッチで複数の例外をまとめて処理する
Java 7以降では、複数の例外に対して同じ処理を行いたい場合、|(パイプ)演算子で例外クラスを結合する「マルチキャッチ」構文が使えます。
try {
// ... 例外が発生する可能性のある処理 ...
} catch (IOException | SQLException e) {
// IOExceptionとSQLExceptionを同じ処理で捕捉
System.out.println("外部リソースのエラーが発生しました: " + e.getMessage());
log.error("リソースエラー", e);
}
前述の複数 catch ブロックとの使い分けは明確です。例外ごとに異なる処理が必要なら複数 catch、同じ処理でよいならマルチキャッチを使います。
ただし、継承関係にある例外(例: IOException と FileNotFoundException)を同時に指定するとコンパイルエラーになるため注意が必要です。
Java 22以降では、catchブロックで受け取った例外オブジェクトを使わない場合、変数名を_(アンダースコア)にできる「無名変数」構文が使えます。
try {
Integer.parseInt(token);
} catch (NumberFormatException _) {
// 例外の中身を使わない場合はこう書ける
System.out.println("数値に変換できませんでした。");
}
従来は catch (NumberFormatException e) と書いても e を一切使わないケースがよくありましたが、_ を使うことで「この例外の詳細は使わない」という意図がコード上で明確になります。
finallyブロック:必ず実行される処理
finally ブロックは、try ブロックで例外が発生したかどうかにかかわらず、必ず最後に実行される処理を記述する場所です。
必ず実行される特性は、ファイルハンドルやデータベース接続といった、使用後に必ず解放しなければならないリソースの後処理に役立ちます。
FileReader reader = null;
try {
reader = new FileReader("sample.txt");
// ... ファイルの読み込み処理 ...
} catch (IOException e) {
System.out.println("ファイル処理中にエラーが発生しました。");
} finally {
if (reader != null) {
try {
reader.close(); // ファイルを閉じる
System.out.println("リソースを解放しました。");
} catch (IOException e) {
e.printStackTrace();
}
}
}
この例では、try ブロックで処理が成功しても、catch ブロックでエラーが捕捉されても、finally ブロックは必ず実行されます。
finallyの実行により、readerが確実にクローズされ、リソースの解放漏れを防げます。
try { 処理1try { 処理2catch (IOException e)エラー対応finally後処理(close)try-with-resources文でリソースを自動解放する
Java 7以降では、try-with-resources 文を使うことで、finally ブロックでの手動クローズが不要になります。
AutoCloseable インターフェースを実装したクラス(FileReader、BufferedReader、Connection 等)が対象です。
try の括弧内でリソースを宣言すると、ブロックを抜ける際に自動的に close() が呼ばれます。
// try-with-resourcesを使った例(Java 7以降)
try (FileReader reader = new FileReader("sample.txt");
BufferedReader br = new BufferedReader(reader)) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
System.out.println("ファイル処理中にエラーが発生しました。");
}
// finallyブロックもclose処理も不要!
前述の finally で手動クローズする例と比べて、コードが大幅に簡潔になります。
複数のリソースをセミコロン区切りで宣言でき、宣言の逆順で自動クローズされます。
2026年時点の最新LTSであるJava 25でも、リソース管理には try-with-resources を使うのが標準的なベストプラクティスです。
Java 9以降では、try ブロックの外で宣言した実質final変数(事実上変更されない変数)をそのまま try-with-resources の括弧内で使えるようになりました。
Java 7〜8では try の括弧内で新たに変数を宣言する必要がありましたが、Java 9以降ではより柔軟な記述が可能です。
従来のfinallyでの手動クローズには、close()自体が例外を投げると、本来の例外が上書きされてしまうという罠がありました。try-with-resourcesはこの問題も自動で解決し、抑制された例外(suppressed exception)として保持してくれます。
ただし、try-with-resourcesが自動化するのはリソースのclose()呼び出しだけです。IOExceptionなどの検査例外そのものは、従来どおりcatchするかthrowsで宣言する必要があります。
Javaの例外をスローする:throwとthrowsの違い
例外処理は、例外を捕捉するだけではありません。自ら例外を発生させたり、メソッドの呼び出し元に処理を委ねたりすることも重要なテクニックです。
throwは例外を発生させる文、throwsは処理の責任を呼び出し元に渡す宣言です。それぞれの役割を順に見ていきましょう。
例外を意図的に発生させる:throw文
throw 文を使用すると、プログラマーが意図的に例外を発生させられます。
これは、メソッドの引数が不正である場合など、特定の条件を満たしたときにプログラムをエラー状態にしたい場合に使用します。
public void setAge(int age) {
if (age < 0) {
// 不正な値が設定されようとしたため、例外をスローする
throw new IllegalArgumentException("年齢に負の値は設定できません。");
}
this.age = age;
}
このメソッドは、引数 age が負の値の場合に IllegalArgumentException をスローします。
IllegalArgumentExceptionをスローすることで、不正なデータが設定されるのを防ぎ、呼び出し元に問題を通知できます。
メソッドがスローする可能性のある例外:throws句
throws 句は、メソッドのシグネチャ(定義部分)に記述し、そのメソッドが処理せずに呼び出し元へ投げる可能性のある Checked Exception を宣言するものです。
// このメソッドはIOExceptionをスローする可能性があることを宣言
public void readFile(String filePath) throws IOException {
FileReader reader = new FileReader(filePath);
// ... 読み込み処理 ...
reader.close();
}
readFileの呼び出し側は、IOExceptionがスローされる可能性を認識する必要があります。
try-catchで処理するか、throws句でさらに上の呼び出し元へ処理を委譲します。
このように throws 句は、例外処理の責任を呼び出し元に明示的に伝える役割を果たします。
throwsとtry-catchの使い分け
検査例外の処理方法は、try-catchとthrowsの2つです。選ぶ基準は「そのメソッド内でエラーに対処できるか」です。
なお、throws Exceptionのように広く宣言するのは避け、スローする例外は具体的なクラスで宣言します。呼び出し元にすべての検査例外の処理を強制し、例外ごとの意味が失われるためです。
例外の再スロー
catch ブロックで例外を一度捕捉した後、ログ出力などの処理を行い、再び同じ例外や別の例外を throw することを「例外の再スロー」と呼びます。
public void processData() throws MySystemException {
try {
// ... 何らかの処理 ...
} catch (SQLException e) {
// エラーログを記録する
log.error("データベースアクセス中にエラーが発生しました。", e);
// より上位のアプリケーション例外に変換して再スローする
throw new MySystemException("システムエラーが発生しました。管理者に連絡してください。", e);
}
}
再スローにより、下位レイヤーの技術的例外(SQLException等)を上位レイヤーのアプリケーション例外(MySystemException等)に変換できます。
システムの各層で関心事を分離でき、コードの保守性が高まります。
例外情報を失わない|causeとして元の例外を渡す
下位の例外を捕捉して上位の例外に変換するときは、元の例外をcauseとして渡します。渡さないと、根本原因のスタックトレースが失われてしまいます。
// 悪い例: 元の例外情報が失われる
catch (SQLException e) {
throw new MySystemException("データベースエラー");
}
// 良い例: causeとして元の例外を渡す
catch (SQLException e) {
throw new MySystemException("データベースエラー", e); // 第2引数で元の例外を渡す
}
元の例外をcauseとして設定することで、根本原因のスタックトレースが残り、デバッグが容易になります。
Javaカスタム例外の作り方と実践例
Javaが提供する既存の例外クラスだけでは、アプリケーション固有のエラー状況を十分に表現できないことがあります。
そのような場合に、独自の例外クラス(カスタム例外)を作成します。
なぜカスタム例外が必要なのか?
カスタム例外を作成する主な理由は、エラーの原因をより明確に伝えるためです。
例えば、銀行システムで「残高不足」が発生した場合、InsufficientBalanceExceptionという独自の例外を定義すれば、エラーの意味が格段にわかりやすくなります。
IllegalArgumentExceptionのような汎用例外では、何が起きたのか名前だけでは判断できません。
カスタム例外には、以下のメリットがあります。
- 可読性の向上: 例外クラス名だけで、どのようなエラーかが直感的に理解できます。
- 詳細なエラー情報: 独自のフィールドを追加して、残高や不足額などの詳細情報を例外に含められます。
- 柔軟なエラーハンドリング: 例外の種類ごとに
catchブロックを分け、それぞれに特化したエラー処理を実装できます。
カスタム例外クラスの作成方法
カスタム例外クラスの作成は簡単です。
Exception クラス(Checked Exceptionの場合)または RuntimeException クラス(Unchecked Exceptionの場合)を継承するだけです。
エラーメッセージを受け取るコンストラクタと、原因となった例外を受け取るコンストラクタの2つを定義します。
// Checked Exceptionとしてカスタム例外を作成する例
public class InsufficientBalanceException extends Exception {
// エラーメッセージを受け取るコンストラクタ
public InsufficientBalanceException(String message) {
super(message);
}
// エラーメッセージと原因となった例外を受け取るコンストラクタ
public InsufficientBalanceException(String message, Throwable cause) {
super(message, cause);
}
}
回復を試みるべきエラー(例: ネットワークの一時障害)はChecked Exceptionに、修正すべきバグ由来のエラー(例: 不正なデータ)はUnchecked Exceptionにする、という設計指針で判断します。
カスタム例外の利用例
作成したカスタム例外は、throw 文を使ってスローします。
public void withdraw(double amount) throws InsufficientBalanceException {
if (this.balance < amount) {
// 残高が足りない場合、カスタム例外をスローする
throw new InsufficientBalanceException("残高が " + (amount - this.balance) + " 円不足しています。");
}
this.balance -= amount;
System.out.println(amount + " 円を出金しました。");
}
// 呼び出し元の処理
try {
account.withdraw(50000);
} catch (InsufficientBalanceException e) {
System.out.println("出金エラー: " + e.getMessage());
}
このようにカスタム例外を利用することで、アプリケーションの業務ロジックに沿った、わかりやすい例外処理を実現できます。
検査例外と非検査例外の使い分け
理論を学んだところで、次は実践的な使い分けについて考えます。適切な例外設計は、APIの使いやすさやシステムの保守性に直結します。
ここでは、回復できるかどうかという判断基準と、Spring Bootでの実例を紹介します。
適切な例外設計の考え方|回復できるかで決める
検査例外と非検査例外を使い分ける際の最も重要な判断基準は、「呼び出し元(APIの利用者)が、そのエラーから回復できるか」です。
検査例外を使うべきケース
- 呼び出し元がエラーから回復できる、あるいは代替処理を行うことが期待される場合。
- 例: 「指定されたファイルが存在しない(
FileNotFoundException)」→ 呼び出し元は「デフォルト設定で処理を続行する」「ユーザーに別ファイルの選択を促す」などの対応が考えられる。
非検査例外(RuntimeException)を使うべきケース
- プログラムの事前条件を満たしていないなど、明らかにプログラマのバグが原因の場合。
- 例:
nullが渡されるべきでない引数にnullが渡された(NullPointerException)。これは呼び出し側のコードを修正すべき問題であり、その場で回復しようとするべきではない。
APIを設計する際は、「このエラーが起きたとき、API利用者にどうしてほしいか?」を自問自答することが、適切な例外選択の鍵となります。
業務システム開発における検査例外の実例
業務システムでは、独自の検査例外を定義することがよくあります。たとえば会計系のシステムなら、「仕訳バランスエラー」「会計期間締め後の更新」といった業務ルール違反を独自の検査例外として定義し、Controllerで@ExceptionHandlerに拾わせる設計が考えられます。
「予期すべきエラー」を検査例外で型として明示しておくと、コードレビュー時に処理漏れを発見しやすく、実装者が業務ルールを忘れにくいという副次効果もあります。
銀行の振込処理でいえば、「残高不足」はシステム障害ではなく、業務ルール上の正常な結果です。前述のInsufficientBalanceExceptionのように、業務上のルールに関わるエラーは、検査例外で表現すると扱いやすくなります。呼び出し元はcatchして、「残高が不足しています」とユーザーに表示できます。
Spring Bootでの検査例外とグローバル例外処理
Spring Bootで業務アプリを作る場合、@ControllerAdvice と @ExceptionHandler を使ったグローバル例外処理が定番です。
検査例外はサービス層でビジネス例外として投げ、Controller層では握らず、@ControllerAdvice でHTTPレスポンスに変換する、という形にまとめられます。
Spring Boot 3系(Jakarta EEへの名前空間移行後)でもこの設計思想自体は変わっておらず、2026年時点でも通用する定番パターンです。
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(InsufficientBalanceException.class)
public ResponseEntity<ErrorResponse> handle(InsufficientBalanceException e) {
return ResponseEntity.badRequest()
.body(new ErrorResponse("BALANCE_SHORT", e.getMessage()));
}
}
この構造を採用すると、Controllerごとにtry-catchを書く必要がなくなり、横断的関心事(ロギング・監査・エラーレスポンスの統一)を一箇所に集約できます。
実務のSpring Boot案件では、ビジネス例外は検査例外で定義し、インフラ例外(DB接続断など)は非検査例外で扱う、という住み分けで設計する方法があります。
検査例外を避けるべきケース|ラムダ式・ストリームAPI
検査例外は強力な仕組みですが、現代のJavaプログラミング、特にラムダ式やストリームAPIと組み合わせる際には、扱いにくさが問題になることがあります。
ラムダ式・ストリームAPIと検査例外の相性問題
Java 8(2014年リリース)で導入されたストリームAPIやラムダ式は、2026年時点でも日常的に使われる主力機能です。
しかし、これらの多くが受け入れる関数型インタフェース(例: Function, Predicate)のメソッドシグネチャにはthrows宣言が含まれていないため、ラムダ式内で検査例外を直接スローできません。
そのため、ラムダ式の本体から検査例外をスローしようとすると、コンパイルエラーになってしまいます。
List<String> fileNames = List.of("file1.txt", "file2.txt");
// コンパイルエラー!
// ラムダ式内でIOExceptionをスローできない
fileNames.stream()
.map(name -> new FileReader(name)) // FileReaderのコンストラクタはFileNotFoundExceptionをスローする
.collect(Collectors.toList());
この「相性の悪さ」は、Javaコミュニティでも長年議論されているテーマです。
ラップしてRuntimeExceptionに変換するパターン
この問題を解決する一般的なパターンが、検査例外をtry-catchで捕捉し、非検査例外でラップして再スローする方法です。
fileNames.stream()
.map(name -> {
try {
return new FileReader(name);
} catch (FileNotFoundException e) {
// 検査例外を非検査例外でラップしてスローする
throw new UncheckedIOException(e); // Java 8で追加された便利な非検査例外
}
})
.collect(Collectors.toList());
この方法を使えば、ラムダ式内で検査例外を扱うことができ、コードの記述がスムーズになります。
ただし、このパターンを乱用すると、検査例外が持つ「回復可能なエラーを呼び出し元に通知する」という本来のメリットが失われてしまいます。
あくまで、ラムダ式など構造上やむを得ない場合に限定して利用するのが賢明です。
なお、Java 8以降に追加されたUncheckedIOExceptionのほか、Spring Framework系ではDataAccessExceptionのようにチェック例外を非チェック例外に変換するラッパが標準で用意されており、手動でラップする前に既存ユーティリティの有無を確認するのがおすすめです。
実務では、意味もなくRuntimeExceptionで直接ラップするコードをよく見かけます。専用のラッパークラスがある場合はそちらを使うほうが、例外の種類から発生源の見当が付けやすくなります。
Java例外処理のベストプラクティス
最後に、品質の高いコードを書くための Javaの例外処理 に関するベストプラクティスをいくつか紹介します。
これらの指針を意識することで、より堅牢で保守性の高いアプリケーションを構築できます。
例外を適切に処理する・ログに出力する・戦略を立てる、の3点から見ていきます。
例外を適切に処理する|避けたいアンチパターン
catchブロックを空にしない:エラーを握りつぶす行為は、問題の発見を遅らせる最大の原因です。最低でもログに出力するなど、何らかの対応を必ず行います。Exceptionで安易に捕捉しない:catch (Exception e)は、予期しない実行時例外まで捕捉してしまいます。可能な限り、IOExceptionやSQLExceptionのような具体的な例外クラスで捕捉します。NullPointerExceptionやClassCastExceptionといった、本来修正すべきバグまで捕捉すると、問題の発見が本番リリース後にずれ込みがちです。- 回復処理を検討する:単にエラーメッセージを表示して終了するだけでなく、処理をリトライする、デフォルト値を返す、ユーザーに再入力を促すなど、アプリケーションを継続させる回復処理を検討します。
ログがなければ、原因調査は手動でソースを追うしかありません。私の経験でも、半日かかるバグ調査は珍しくありませんでした。今はClaude Codeにログとスタックトレースを渡せば、すぐに当たりがつきます。ただし、それも「そもそもログが出力されている」ことが大前提です。
例外をログに出力する
エラーが発生した原因を調査する上で、ログは最も重要な情報源です。
- ロギングフレームワークを利用する:
e.printStackTrace()は、開発中のデバッグには便利ですが、コンソールに直接出力されるため、本番環境のログ管理には不向きです。SLF4JやLog4j2といった、標準的なロギングフレームワークの利用を強く推奨します(Log4j2は2021年のLog4Shell以降も脆弱性報告が継続しているため、必ず最新バージョンを使用し、定期的なアップデートを心がけてください)。 - 十分な情報を記録する: ログには、タイムスタンプ、エラーレベル(ERROR, WARNなど)、エラーメッセージ、そしてスタックトレースを含めるのが基本です。これにより、いつ、どこで、なぜエラーが発生したのかを後から追跡できます。
catch (SQLException e) {
// ログレベルERRORで、スタックトレースを含めて例外情報を記録
log.error("顧客データの取得に失敗しました。userID: {}", userID, e);
}
独自の例外処理戦略を立てる
プロジェクトやアプリケーション全体で、例外処理に関する一貫したルール(戦略)を立てることが重要です。
- 例外の変換ルール: どの層で技術的例外(
SQLExceptionなど)を捕捉し、どの層で業務的例外(カスタム例外)に変換するのかを決めます。 - ログ出力のレベル: どのようなエラーを
ERRORレベルで記録し、どのようなものをWARNレベルで記録するのか、基準を明確にします。 - ユーザーへの通知方法: 例外が発生した際に、ユーザーにどのようなメッセージを表示するのか、画面遷移をどうするのか、といったUI/UXに関わる部分も設計に含めます。
これらの戦略をチームで共有し、コーディング規約として定めておくことで、アプリケーション全体の品質を均一に保ち、メンテナンス性を向上させられます。
たとえば、Repository層でSQLExceptionをキャッチしたらDataAccessExceptionに変換し、Service層で業務ルール違反ならBusinessExceptionをスローし、Controller層で最終的にHTTPステータスコードにマッピングする、という決め方があります。
このルールをチームで共有しておけば、例外処理に関するコードレビューの指摘を減らせます。
よくある質問
Q1. Javaの例外処理はなぜ必要ですか?
A1. プログラムの強制終了を防ぎ、安全にエラーへ対応するために必要です。例外処理がないと、エラー発生時にプログラムが即座に停止し、入力中のデータが失われたりシステムが不安定になったりします。
Q2. 検査例外(Checked Exception)と非検査例外(Unchecked Exception)の違いは何ですか?
A2. コンパイラが処理を強制するかどうかです。検査例外(IOException等)は、try-catchかthrowsのどちらかを書かないとコンパイルエラーになります。非検査例外(RuntimeExceptionとError)には、その強制がありません。判定に迷ったら、そのクラスの継承チェーンを遡ってRuntimeExceptionにたどり着くかを確認します。
Q3. try-catchとtry-catch-finallyの違いは何ですか?
A3. try-catchは例外の捕捉と対応のみを行いますが、try-catch-finallyのfinallyブロックは、例外の有無にかかわらず必ず実行される処理を追加できます。ファイルやデータベース接続などリソースの後処理に使います。
Q4. throwとthrowsの違いは何ですか?
A4. throwは例外を実際に発生させる文で、throwsはメソッドのシグネチャに記述し、そのメソッドが例外をスローする可能性があることを宣言する句です。throwは実行文、throwsは宣言という違いがあります。
Q5. カスタム例外はいつ作るべきですか?
A5. 既存の例外クラスだけではアプリケーション固有のエラー状況を十分に表現できない場合に作成します。例外クラス名だけでエラーの意味が伝わり、独自のフィールドで詳細情報を持たせられるメリットがあります。
Q6. catchブロックは空にしてもいいですか?
A6. いいえ、避けるべきです。空のcatchブロックはエラーを握りつぶし、後の工程でより深刻な問題を引き起こす原因になります。最低でもログ出力など、何らかの対応を行ってください。
Q7. 検査例外は時代遅れですか?
A7. Kotlinなど新しいJVM言語は検査例外を採用していないため、「時代遅れ」と語られることがあります。ただしJava本体では、2026年時点で検査例外を廃止する計画は公表されておらず、IOException・SQLExceptionなど標準APIの大部分が依然として検査例外のままです。業務例外の一部に限って検査例外を使い、インフラ系は非検査例外に寄せる、というハイブリッド運用が現実解です。
Q8. throws Exception と書くのはアリですか?
A8. 学習用のスクリプトなら許容範囲ですが、業務コードではNGです。throws Exceptionは、呼び出し元にすべての検査例外処理を強制し、例外ごとの意味を失わせます。呼び出し側がcatch (Exception e)でまとめて受けると、RuntimeExceptionまで巻き込んで握りつぶすリスクもあるため、スローする例外は具体クラスで宣言するのが鉄則です。
Q9. try-with-resourcesを使えば検査例外の処理は不要になりますか?
A9. いいえ、不要にはなりません。try-with-resourcesが自動化するのはリソースのclose()呼び出しだけで、IOException等の検査例外自体は、従来どおりcatchするかthrowsで宣言する必要があります。ただしfinallyで手動クローズする旧来パターンに比べ、抑制された例外(suppressed exception)を自動で扱ってくれる分、安全に書けます。
Q10. ラムダ式の中で検査例外をそのままスローできないのはなぜですか?
A10. FunctionやPredicateなど標準の関数型インタフェースのメソッドシグネチャに、throws宣言が含まれていないためです。ラムダ式内で検査例外が発生する処理を呼ぶ場合は、try-catchで捕捉してUncheckedIOException等の非検査例外でラップしてから再スローするのが一般的なパターンです。
Q11. Java資格試験では検査例外はどう問われますか?
A11. Java SE 21 Developer認定資格(Oracle公式の試験コードは1Z0-830)では、検査例外と非検査例外の区別、throws宣言の継承時のルール、マルチキャッチの制約などが頻出です。特に「オーバーライド時に親メソッドより広い検査例外をthrows宣言できるか」は定番のトピックです。なお、日本ではOracle認定Javaプログラマが「Bronze」「Silver」「Gold」の区分で提供されており、Silver・GoldはJava SE 11・17対応の日本語試験が現在も受験できます(Java SE 11対応は2026年11月30日、Java SE 17対応は2027年2月28日に配信終了の予定で、Java SE 25対応の試験が2026年11月下旬から配信される予定と日本オラクルが案内しています)。1Z0-830はグローバルのJava SE 21 Developer認定資格の試験コードで、日本語のSilver・Goldとは別の試験です。
まとめ:Javaの例外処理を使いこなそう
Javaの例外処理は、単なるエラー対応の技術ではなく、システムの安定性と信頼性を確保するための中心的な要素です。
try-catch-finallyの基本構造を理解し、throwとthrowsで責任を適切に分担しましょう。
カスタム例外でアプリ固有のエラーを明確に表現し、ログ出力や回復処理のベストプラクティスを日々のコーディングで実践することで、堅牢なJavaアプリケーションを開発できるようになります。
「例外は型で語る」という設計思想を理解すれば、検査例外は強制されるルールではなく、コードの品質を底上げする武器になります。
この記事のポイントをまとめます。
- 例外処理はプログラムの強制終了を防ぎ、安全にエラーへ対応する仕組み
- 検査例外(Checked)はコンパイラが処理を強制する。呼び出し元が回復できるエラーに使う(
IOException等) - 非検査例外(Unchecked)は実行時に発生する。バグ由来なので、コードを直して防ぐ(
NullPointerException等) - try-catch-finallyやtry-with-resourcesで、捕捉・対応・後処理を行う
- throwで例外を発生させ、throwsで呼び出し元に処理を委譲する。
throws Exceptionは避ける - カスタム例外でアプリ固有のエラーを表現し、元の例外は
causeとして渡す - ラムダ式では検査例外を直接スローできない。ラップは構造上やむを得ない場合に限る
- catchブロックを空にしない、具体的な例外で捕捉する、ログを記録する
なお、2026年時点の最新LTSはJava 25(2025年9月リリース)です。
本記事で紹介したtry-catch・throw/throws・カスタム例外の基本構文は、Java 25でもそのまま通用します。
| REV. | DATE | 内容 |
|---|---|---|
| 6 | 2026-10-16 | 図解を描き直し(図面のデザインにそろえた) |
| 5 | 2026-10-14 | 一人称と言い回しの表記をそろえた。雰囲気だけの挿絵を外した |
| 4 | 2026-10-04 | 記載内容の事実確認と修正 |
| 3 | 2026-09-04 | 2026年9月時点の情報で全体を見直し(古い記述を5件更新) |
| … ほか1回の改訂 | ||
| 1 | 2025-10-18 | 初版 |






