はじめに
Javaの例外処理は、想定外の入力、ファイル読み込みの失敗、数値変換エラー、null参照などに対して、プログラムを安全に分岐させるための仕組みです。初心者はtry、catch、finallyの役割を押さえるだけでも、エラーで処理全体が止まる場面を減らせます。
一方、上級者に近づくほど、例外を握りつぶさない設計、原因例外を残す連鎖、try-with-resourcesによるリソース管理、ログに残す情報の粒度が課題になります。そのため、単に構文を覚えるだけでなく、どの例外をどこで捕捉し、どこへ伝播させるかを整理することがJavaの例外処理の軸になるのが基本です。
公式ドキュメントによれば、Javaの例外はThrowableを起点にExceptionとErrorへ分かれます。詳細な言語仕様やAPIの確認には、Oracle Java TutorialsのExceptionsと、Java SE 21のThrowable APIが一次情報として役立ちます。
- Java SE 21 / OpenJDK 21系を想定
- 標準ライブラリのみ使用し、外部フレームワークには依存しません
- コンソール出力は環境や引数、ファイルの有無で変わります
- Javaの例外処理における検査例外、実行時例外、エラーの違い
try-catch、finally、マルチキャッチの使い分け- サンプルコードで理解するカスタム例外、例外連鎖、再スローの実装
- 初心者が避けたい捕捉範囲の広すぎるコードと、上級者向けの設計観点
- Java例外処理を保守しやすくするガイドとテクニック
Javaの例外処理とは
結論から言えば、Javaの例外処理は「正常系から外れた状態」を通常の戻り値とは別経路で扱う構文とクラス群です。try内で失敗し得る処理を書き、catchで型ごとの対応を分け、必要に応じてthrowやthrowsで呼び出し元へ責任を渡します。
これにより、Javaの処理はエラーの場所と対応方針をコード上で分けて表現できます。ただし、例外を何でも捕捉すればよいわけではなく、復旧できる例外だけを処理し、復旧できない状態は適切に伝播させる判断が求められますし、ここがポイントです。
| 分類 | 代表例 | 扱い方 | 向く場面 |
|---|---|---|---|
| 検査例外 | IOException | catchまたはthrowsが必要 | ファイル、通信、DBなど外部要因 |
| 実行時例外 | NullPointerException | コンパイル時の強制はありません | 不正な引数、状態不整合、バグの検出 |
| エラー | OutOfMemoryError | 通常はアプリ側で捕捉しません | JVMや環境レベルの重大な異常 |
| カスタム例外 | CustomException | 業務や処理単位の失敗を表現 | 呼び出し元に意味ある失敗理由を返す場面 |
| 例外連鎖 | Throwableのcause | 原因例外を保持して再スロー | 低レイヤーの失敗を上位概念へ変換する場面 |
例外処理の基本概念
例外は、通常の処理手順では続行できない状態を表すオブジェクトです。その状態をThrowable系の型として投げることで、Javaは呼び出し階層をさかのぼりながら対応できるcatchを探します。
このとき、初心者がつまずきやすいのは「例外が発生した行で処理が止まり、同じtryブロック内の後続処理は原則として進まない」という点です。そのため、失敗しても続けたい処理と、失敗したら中断すべき処理を同じtryに詰め込みすぎない構成が扱いやすくなります。
一般に、検査例外は外部環境の変化に備える設計と相性がよく、実行時例外はプログラム内部の前提崩れを表す場面で使われますが、これは押さえたい点です。たとえばFileReaderの失敗は利用者のファイル配置に左右されますが、Integer.parseIntへ数値でない文字列を渡す失敗は入力検証の不足として扱えます。
Javaにおける例外の種類
Javaの例外階層は、Throwable、Exception、RuntimeException、Errorの関係で整理すると理解しやすくなります。Exception配下のうちRuntimeExceptionではないものが検査例外として扱われ、コンパイラが処理漏れを検出するのが目安です。
一方、RuntimeException配下は非検査例外であり、throws宣言がなくてもコンパイルできます。ただし、宣言が不要であることは対応が不要という意味ではなく、入力検証、事前条件チェック、単体テストで発生源を減らすのが現実的です。
検査例外(Checked Exceptions)
検査例外は、コンパイル時に処理が求められる例外です。代表例にはIOException、SQLException、ClassNotFoundExceptionがあり、ファイル、データベース、クラス読み込みなど外部条件に左右される処理でよく登場します。
そのため、呼び出し元で復旧できるならcatchし、呼び出し元の判断に委ねるならthrowsで明示するのがポイントです。Javaのガイドとしては、例外の意味を失わない範囲で上位層へ伝える設計が読みやすくなります。
実行時例外(Runtime Exceptions)
実行時例外は、コンパイラが捕捉を強制しない例外です。代表例にはNullPointerException、ArrayIndexOutOfBoundsException、IllegalArgumentException、NumberFormatExceptionがあります。
これらは、データの前提や呼び出し方が崩れたときに発生しやすい種類です。具体的には、nullを許容しない引数にnullが入る、配列の長さを超えて参照する、数値でない文字列を数値化する、といった失敗が該当するのが一般的です。
エラー(Errors)
Errorは、アプリケーションコードだけで通常復旧しにくい重大な問題を表します。代表例にはOutOfMemoryErrorやStackOverflowErrorがあり、メモリ不足や過剰な再帰など実行環境に近い層の異常を示します。
ただし、Errorを広くcatchして処理を継続すると、状態が壊れたまま動き続ける危険があるのが現実的です。そのため、通常のJavaアプリケーションではErrorを業務ロジックの例外処理対象に含めない方針が一般的です。
例外処理の基本手法
Javaの例外処理の基本手法は、try-catchで発生箇所と対応箇所を分け、必要な後処理をfinallyまたはtry-with-resourcesへ寄せる形です。サンプルコードを読むときは、どの行で例外が起き、どのcatchへ制御が移るかを追うと理解しやすくなります。
そのため、初心者向けの最初のテクニックは、例外型を広げすぎないことです。Exceptionでまとめて捕捉する前に、ArithmeticExceptionやIOExceptionのように発生理由が読み取れる型を選ぶと、保守時の判断材料が残ります。
try-catch文の使用方法
try-catch文は、Javaの例外処理で最もよく使われる形です。tryには失敗し得る処理を置き、catchには復旧、通知、代替値の設定、ログ出力など失敗後の扱いを書きますし、これが一つの目安です。
この構文では、catchに書いた例外型と発生した例外型が一致するか、その親型である場合に捕捉されます。ただし、親型を先に書くと子型のcatchへ到達できなくなるため、複数のcatchを使う場合は具体的な型から並べる必要があります。
サンプルコード1:基本的なtry-catch文の使用例
次のサンプルコードは、整数の除算で0除算が起きる場面を扱いると整理できます。Javaでは整数を0で割るとArithmeticExceptionが発生するため、catchで利用者向けのメッセージに変換しています。
結果: 期待される出力は「0での除算はできません。」です。
このコードでは、number / divisorの評価中にArithmeticExceptionが発生します。その時点でSystem.out.println(result)には進まず、対応するcatchへ制御が移ります。
ただし、実務的な入力処理では、除算の直前にdivisor == 0を判定してエラーメッセージを返すほうが自然な場合もあると理解できます。例外処理は失敗後の制御に向きますが、予測しやすい不正入力は事前チェックと組み合わせると読みやすくなります。
try-catch-finally文の詳細
finallyは、例外の発生有無にかかわらず実行される後処理用のブロックです。ファイルやソケットのクローズ、ロックの解放、一時状態のリセットなど、成功時と失敗時の両方で必要な処理を置く場所として使われてきました。
一方、Java 7以降はAutoCloseableを実装したリソースならtry-with-resourcesで自動クローズできます。そのため、現在のJavaで新規にリソース管理を書くなら、finallyよりtry-with-resourcesを優先する場面が多くなると覚えるとよいでしょう。
サンプルコード2:finally節を持つ例外処理
次のサンプルコードは、tryで例外が起きた後もfinallyが実行される流れを示します。catchでメッセージを出した後、最後に共通の後処理メッセージを出力します。
結果: 期待される出力は、割り算エラーのメッセージに続いて、finally内のメッセージが表示される形です。
このJavaコードでは、10 / 0の時点でArithmeticExceptionが発生します。その後、catchでエラーメッセージを出し、finallyで共通処理を走らせます。
結果: 期待される出力例は、上のように2行のメッセージが並ぶ形式です。
ただし、finallyの中でreturnや別の例外を発生させると、元の例外情報が見えにくくなる場合があります。そのため、finallyには単純な解放処理を置き、失敗理由の変換や分岐はcatch側へ寄せるほうが安全です。
複数の例外をキャッチする方法
複数の失敗理由に対して同じ処理を行う場合、Java 7以降のマルチキャッチが使えます。catch (A | B e)の形にすると、AまたはBのどちらが発生しても同じブロックで処理できると考えられます。
このテクニックは、ログ出力やエラーレスポンスの組み立てが同じ場合に効果を発揮します。一方、例外ごとに復旧方法が違う場合は、無理にまとめず個別のcatchへ分けるほうが読みやすい構成になります。
サンプルコード3:複数の例外をキャッチするコード例
次のサンプルコードでは、引数がない場合と、引数の内容が空文字の場合をマルチキャッチで処理していると言えるでしょう。初心者向けには少し不自然な条件分岐に見えますが、|で複数型をまとめる構文の確認には使いやすい例です。
結果: 引数なしで起動した場合の期待される出力は「エラー: 引数が不足しています。」です。
このJavaコードでは、IllegalArgumentExceptionとArrayIndexOutOfBoundsExceptionを同じcatchで扱いるのが基本です。そのため、エラーメッセージの出し方が同じであれば、重複したSystem.out.printlnを書かずに済みます。
ただし、IllegalArgumentExceptionは不正な引数を表す実行時例外であり、配列の範囲外を表すArrayIndexOutOfBoundsExceptionとは意味が異なります。上級者向けの設計では、同じ処理にまとめる前に、例外の意味が利用者や保守者に伝わるかを確認するのが目安です。
catch (Exception e)で広く捕捉すると、想定外のバグまで同じ扱いになります。復旧できる例外型を中心に選び、原因が不明な例外はログや上位層で追える形に残します。Javaの例外処理の応用技術
基本構文が分かると、Javaの例外処理は「失敗をどういう意味で表すか」という設計に移りますし、ここがポイントです。アプリ固有の失敗をCustomExceptionとして表し、原因例外を保持しながら上位層に伝えると、呼び出し元は低レイヤーの詳細に引きずられず判断できます。
そのため、応用技術では、独自の例外クラス、例外の連鎖、再スローの扱いが中心になります。これらのテクニックは初心者にも読める構文で書けますが、設計上の意図が欠けると上級者でも追跡しにくいコードになるのがポイントです。
独自の例外クラスの作成方法
独自の例外クラスは、標準例外だけでは失敗の意味が伝わりにくい場合に使います。たとえば、ファイル読み込みの失敗をそのままIOExceptionとして上位層へ出すより、設定ファイルの読み込み失敗を表す独自例外へ変換したほうが、呼び出し元の判断が明確になります。
基本的には、検査例外として扱いたいならExceptionを継承し、実行時例外として扱いたいならRuntimeExceptionを継承するのが一般的です。ただし、何でも独自例外にすると型が増えすぎるため、呼び出し元の分岐やログ分析に意味がある単位へ絞ることが大切です。
サンプルコード4:カスタム例外クラスの作成と使用
次のサンプルコードは、検査例外としてCustomExceptionを作る最小構成です。コンストラクタでStringのメッセージを受け取り、親クラスのsuper(message)へ渡しています。
結果: 期待される状態は、CustomExceptionという独自の検査例外クラスが定義されることです。
このクラス単体では例外は発生しません。実際に使う側でthrow new CustomException(...)を行うことで、Javaの例外処理の流れに乗ります。
結果: 期待される出力は、CustomExceptionのスタックトレースと「独自の例外が発生しました」というメッセージを含む形式です。
このサンプルコードでは、throwで独自例外を発生させ、catchで同じ型を捕捉します。printStackTrace()は学習用には流れを追いやすい一方、本番コードではロギングフレームワークへ渡す形が一般的です。
ちなみに、独自例外にはserialVersionUIDを付けることがあります。直列化を使わない小さな学習コードでは省略されがちですが、警告を避けたいプロジェクトではクラス内に明示しておくとよいでしょう。
例外の連鎖とその取り扱い
例外の連鎖は、低い層で起きた例外を原因として保持したまま、高い層の意味を持つ例外へ変換する考え方です。JavaではThrowableのコンストラクタにcauseを渡すことで、元の例外情報を失わずに再スローできるのが現実的です。
これにより、呼び出し元には「設定読み込みに失敗した」と伝えつつ、ログには「内部的にはファイルが存在しなかった」などの原因を残せます。ただし、原因例外を捨てて新しい例外だけを投げると、調査時に発生源が追いにくくなります。
具体的には、new HighLevelException("...", e)のように、捕捉した例外eを第2引数へ渡すると整理できます。この書き方はJava例外処理のガイドとして押さえたいテクニックであり、上級者向けの保守設計でも頻出します。
サンプルコード5:例外の連鎖を利用したコード
次のサンプルコードは、methodBで発生した低レベルの例外を、methodAで高レベルの例外へ変換します。原因例外をコンストラクタへ渡しているため、スタックトレースには連鎖した情報が残りますが、これは押さえたい点です。
結果: 期待される出力は、HighLevelExceptionの情報と、その原因であるLowLevelExceptionの情報を含むスタックトレースです。
このコードでは、methodB()がLowLevelExceptionを投げ、methodA()がそれを捕捉します。その後、HighLevelExceptionを作る際に原因として渡すため、上位のmainでも低レベルの発生源を追跡できます。
一方、throw new HighLevelException("メソッドAからの例外", null)のように原因を失う書き方は避けるべきです。原因例外を残すことは、障害調査、ログ分析、テスト失敗の切り分けに直結すると理解できます。
例外の再スロー技法
例外の再スローは、catchで一度捕捉した例外を、処理を加えた後に再び投げるテクニックです。ログを残す、メトリクスを加算する、メッセージを補足する、といった処理を挟んだうえで、最終判断を呼び出し元へ委ねられます。
ただし、捕捉した例外をそのままthrow e;する場合と、新しい例外へ包む場合では意味が変わります。そのまま投げれば型と原因は維持され、新しい例外へ包むなら呼び出し元に伝える抽象度を変えられますし、これが一つの目安です。
サンプルコード6:例外の再スローを行うコード例
次のサンプルコードでは、内部メソッドで例外を捕捉してメッセージを出した後、同じ例外を呼び出し元へ再スローします。Javaの制御フローとして、内側のcatchと外側のcatchの両方が関与する例です。
結果: 期待される出力は、内側で捕捉したメッセージと、外側で捕捉したメッセージが順に表示される形式です。
このサンプルコードでは、methodThatThrowsException()の中で作られたExceptionが、内側のcatchを通って再び投げられます。その例外をmainのcatchが受け取るため、同じ失敗に対して階層ごとの処理を加えられます。
結果: 期待される出力例は、上のように内側の処理後、外側の処理が続く2行です。
ただし、すべての階層で同じログを出すと、同一の失敗が複数回記録されてノイズになると覚えるとよいでしょう。そのため、上級者向けの設計では、例外を記録する層と変換する層を分け、ログの重複を避ける構成が選ばれます。
💡 Tips: 例外を変換するときは、元の例外をcauseとして保持します。メッセージだけをコピーすると、発生行や呼び出し経路の情報が失われますが、覚えておくと役立つでしょう。
例外処理の高度なテクニック
高度なテクニックとして特に押さえたいのは、リソースの自動クローズです。ファイル、ストリーム、DB接続のようなリソースは、例外が発生しても確実に閉じる必要があり、Javaではtry-with-resourcesがその役割を担います。
これにより、close()の呼び忘れや、finally内でさらに例外が起きる複雑さを減らせます。ただし、自動で閉じられるのはAutoCloseableまたはCloseableを実装したオブジェクトであり、どのクラスでも使えるわけではありません。
リソースの自動クローズとtry-with-resources文
try-with-resourcesは、tryの丸括弧内でリソースを宣言し、ブロック終了時に自動的にclose()を呼び出す構文です。Java 7で導入され、Java 9以降は事前に宣言済みの実質的にfinalな変数も扱いやすくなりました。
この構文を使うと、ファイル読み込み中にIOExceptionが起きてもBufferedReaderが閉じられます。一方、ファイルパスが存在しない場合や権限がない場合は、読み込み前後のどこかでIOException系の例外が発生します。
サンプルコード7:try-with-resourcesを使用したリソースの安全なクローズ例
次のサンプルコードは、BufferedReaderとFileReaderでテキストファイルを読み込みますし、ここを基本と考えるとよいでしょう。tryの丸括弧内に作成したbrは、処理が正常終了しても例外で抜けてもクローズ対象になります。
結果: 指定したファイルが存在して読み込める場合、期待される出力はファイル内容が1行ずつ表示される形です。読み込みに失敗した場合は、失敗メッセージが標準エラーへ出力されます。
このコードでは、readLine()がnullを返すまで1行ずつ読みます。IOExceptionはファイルが見つからない、読み取り権限がない、読み込み途中で問題が起きる、といった外部要因をまとめて扱いると考えられます。
ただし、path/to/your/file.txtは仮のパスです。動作させる場合は実在するファイルパスに変え、文字コードが問題になる処理ではFiles.newBufferedReaderとCharsetを使う選択もあります。
java.nio.file.Files、Path、StandardCharsets.UTF_8を組み合わせると読み取り条件をコード上に残せます。Java例外処理の注意点とベストプラクティス
Java例外処理の注意点は、例外を「隠さない」「広げすぎない」「意味を変えるなら原因を残す」の三点に集約できると言えるでしょう。初心者はエラーを消す目的でcatchを書きがちですが、上級者向けのコードでは、呼び出し元が判断できる情報を残すことが重視されます。
そのため、例外処理は単なる防御コードではなく、プログラムの失敗モデルを表す設計要素です。サンプルコードを自分のコードへ移すときは、出力メッセージだけでなく、例外型、伝播範囲、復旧可能性を合わせて見直す必要があります。
関連するJava基礎を補うなら、コレクション処理はJava List型完全ガイド、アノテーション設計はJavaアノテーションの12選も参照できるのが基本です。入力や文字列処理で例外が絡む場面では、Javaエスケープ処理の10ステップマスターガイドも補助になります。
適切な例外の選択
適切な例外を選ぶには、発生した問題が入力値の誤りなのか、外部リソースの失敗なのか、プログラム内部の不整合なのかを分けます。たとえば不正な引数ならIllegalArgumentException、状態の不整合ならIllegalStateException、入出力の失敗ならIOExceptionが候補になるのが目安です。
逆に、すべてをRuntimeExceptionやExceptionへ寄せると、呼び出し元が原因に応じて分岐しづらくなります。そのため、Javaの例外処理では、標準例外で意味が足りるなら標準例外を使い、業務上の意味が必要な場合だけ独自例外を検討します。
次のサンプルコードは、null参照で発生するNullPointerExceptionを捕捉しているのがポイントです。学習用としては分かりやすい例ですが、実際の入力処理ではstr == nullを先に判定するほうが読みやすい場合もあります。
結果: 期待される出力は「nullへの参照が発生しました。」です。
このコードでは、str.length()の呼び出し時にstrがnullであるため例外が発生します。ただし、NullPointerExceptionを通常フローの分岐として使うと原因が隠れやすいため、入力検証で避けられる場合は事前チェックを優先するのが一般的です。
同様に、Javaでうるう年を判定する処理のような条件分岐では、例外よりも明示的な判定式が向くことがあります。オーバーライド時の例外宣言については、Javaでマスターするオーバーライドの考え方とも関連します。
例外の正確な伝播
例外の伝播は、発生した例外をどの層で捕捉し、どの層へ渡すかを決める設計です。低い層でユーザー向けメッセージまで作ると再利用しにくくなり、高い層で低レベル例外をそのまま扱うと利用者に不要な詳細が漏れますし、ここがポイントです。
そのため、下位層では原因を保ったまま例外を投げ、上位層では表示、レスポンス、ログ、リトライなど利用者に近い対応を行う構成が扱いやすくなります。Javaのthrows宣言は、そのメソッドが呼び出し元へ渡す可能性のある検査例外を明示する役割も持ちます。
次のサンプルコードは、文字列を整数へ変換する処理でNumberFormatExceptionが発生する例です。convertStringToIntegerが例外を発生させ、main側で利用者向けのメッセージへ変換するのが現実的です。
結果: 期待される出力は「数値変換エラーが発生しました。入力値を確認してください。」です。
このコードでは、Integer.parseInt(input)がtestを整数へ変換できず、NumberFormatExceptionを投げます。その例外をmainのcatchが受け取り、利用者に近い言葉へ置き換えます。
ただし、NumberFormatExceptionは実行時例外なので、throws NumberFormatExceptionの宣言は必須ではありません。学習用に伝播を明示する意図なら使えますが、実務コードでは過剰なthrows宣言が読みやすさを下げないか確認すると整理できます。
基本的に、エラーメッセージは利用者向け、例外メッセージは開発者向け、ログは調査向けと役割を分けます。この切り分けにより、初心者にも分かる表示を保ちつつ、上級者が原因を追えるJava例外処理になります。
使い分けると、入力ミスには再入力を促す処理、外部サービスの一時失敗にはリトライ、プログラム内部の不整合には早期検出という方針が立てやすくなると理解できます。例外処理のガイドとして覚えるなら、復旧できる場所で捕捉し、復旧できない場所では意味を保って伝播させる、という整理が実用的です。
まとめ
Javaの例外処理は、try、catch、finally、throw、throwsを組み合わせて、失敗時の制御を明示する仕組みです。初心者はまず検査例外と実行時例外の違いを押さえ、サンプルコードを通じて制御の流れを追うと理解が進みます。
そのうえで、Javaの上級者向けには、独自例外、例外連鎖、再スロー、try-with-resourcesといったテクニックが設計品質に関わります。例外を隠さず、広げすぎず、原因を残して伝播させることが、保守しやすい例外処理の中心です。
これらのガイドを実装へ移すときは、標準例外で表せる失敗は標準例外を使い、業務上の意味が必要な失敗だけ独自例外へ切り出すると覚えるとよいでしょう。サンプルコードを土台にしながら、入力検証、ログ設計、リソース解放まで含めて考えると、Javaプログラム全体の失敗時の振る舞いを整理できます。
関連記事
- Java List型完全ガイド!初心者でもマスターできる7つのステップ
- Javaアノテーションの12選!初心者から上級者まで徹底ガイド
- Javaでうるう年を判定!初心者でも分かる9ステップ解説
- Javaエスケープ処理の10ステップマスターガイド
- Javaでマスターする!オーバーライドのたった7つのステップ
※本記事は実在のエンジニア複数名で構成される Japanシーモア編集部が、AI支援を活用して作成・校正・公開しています。


