Javaのエラーは、例外の分類とスタックトレースの読み方さえ押さえれば、原因の8割は数分で特定できます。
真っ赤なスタックトレースを見ただけで固まってしまう人は多いはずですが、私もSES時代は半日かけてログとソースコードをにらめっこしていました。
この記事では、頻出する9つの例外の原因と対処法、スタックトレースを最短で読み解く3ステップを、実務でつまずいた経験をもとに整理しました。
Javaのエラーは3種類|まず違いを理解する
Javaのエラーは大きく3種類に分かれます。コンパイルエラー、実行時エラー、論理エラーです。
エラーメッセージを読む前に、自分が今どのエラーと向き合っているのかを区別するだけで、対処のスピードは一気に変わります。
例外(Exception)とは、プログラムの実行中に発生し、正常な処理の流れを妨げる出来事を表すJavaのクラスです。
特に実行時エラーの正体は「例外(Exception)」であり、Javaはこの例外をクラスの継承関係で厳密に管理しています。
継承関係を知らないままcatch文を書くと、思わぬところで例外を取りこぼす原因になるので、まずは全体像から押さえていきましょう。
コンパイルエラーと実行時エラーの違いは何か?
コンパイルエラーはビルド時にJavaコンパイラが検出する文法ミスです。セミコロン忘れや型の不一致が典型で、実行前に必ず気づけます。一方、実行時エラーはプログラムが動いている最中に発生し、コンパイルは通ってしまうのが厄介なところ。
NullPointerExceptionのように、特定の条件でしか顔を出さないバグはここに分類されます。
論理エラーはさらに厄介で、エラーメッセージすら出ないまま「計算結果が違う」形で発覚します。
Exception・RuntimeException・Errorの継承関係
Javaの例外はすべてThrowableクラスを頂点に、ErrorとExceptionの2系統に分かれます。
Errorはメモリ不足やスタックオーバーフローなど、アプリ側では回復できない重大な事象です。
Exceptionはさらにチェック例外と非チェック例外(RuntimeException)に分岐し、日常のバグの大半はRuntimeException側に集中しています。
この継承図を頭に描けるかどうかで、例外処理の設計力は大きく変わります。
チェック例外と非チェック例外の見分け方
見分け方はシンプルで、コンパイラがtry-catchかthrows宣言を強制してくるかどうかです。
IOExceptionやSQLExceptionはチェック例外なので、書かなければコンパイルエラーになります。
対してNullPointerExceptionやArrayIndexOutOfBoundsExceptionは非チェック例外で、書かなくてもビルドは通ってしまう分、実行してから初めて気づきます。
スタックトレースの読み方|原因を追う3ステップ
スタックトレースは、エラーが発生するまでにどのメソッドを経由したかを記録した「地図」です。
ただ闇雲に上から読むのではなく、順番とポイントを押さえるだけで原因特定の時間は大幅に短縮できます。
発生した例外クラス名を最初に確認する
一番上の行には、発生した例外クラス名とメッセージが書かれています。
ここを読むだけで「何が」起きたのかは大半わかるので、まず最初に確認する癖をつけましょう。
NullPointerExceptionなら「何かがnullだった」、ClassCastExceptionなら「型変換に失敗した」と、クラス名自体がヒントになっています。
Caused byを下から遡って根本原因を特定する
複数の例外が連鎖している場合、下の方に「Caused by」という行が出てきます。
このCaused byは根本原因を示していて、実は一番下のCaused byこそが本当のバグの発生源です。
上から読んで見当がつかないときは、先に一番下まで目を通す方が早く原因にたどり着けます。
自分が書いたクラス名を優先的に疑う
スタックトレースにはフレームワークやライブラリのクラス名も大量に混ざります。
すべてを追う必要はなく、自分のパッケージ名(例: com.example)が出てくる行だけを探しましょう。
原因の9割は自分が書いたコードにあり、ライブラリ側のバグであることはまれだというのが、これまでの経験則です。
よく出るJavaの例外9選と原因・対処法早見表
ここからは実務で頻出する例外を、原因と対処法のセットで見ていきます。
名前を知っているだけの例外も、発生パターンさえ押さえれば初見のコードでも数分で原因を潰せるようになります。
早見表の中でも、特に遭遇率が高い4つはコード例つきで見ていきます。
| 例外名 | 主な原因 | 対処法 |
|---|---|---|
| NullPointerException | null参照へのアクセス | nullチェック、Optionalの活用 |
| ClassCastException | 不適切な型変換 | instanceofで事前チェック |
| NumberFormatException | 文字列から数値への変換失敗 | 正規表現での事前検証、try-catch |
| ArrayIndexOutOfBoundsException | 配列の範囲外アクセス | 配列長の事前確認 |
| ConcurrentModificationException | ループ中のコレクション変更 | Iterator.remove()の使用 |
| IOException | ファイル入出力の失敗 | try-with-resourcesの使用 |
| FileNotFoundException | ファイルパスの誤り | パスの存在確認 |
| SQLException | DB接続・クエリの失敗 | 接続情報とクエリ内容の検証 |
| StackOverflowError | 再帰処理の無限ループ | 終了条件の見直し |
NullPointerExceptionが起きる典型パターン
NullPointerExceptionは、初期化されていない変数やメソッドの戻り値がnullのまま、フィールドやメソッドを呼び出したときに発生します。
典型パターンはHashMapのget()結果をそのまま使うケースで、キーが存在しないとnullが返ってくることを忘れがちです。
Java 8以降はOptionalを使えば、nullの可能性をコード上で明示できます。
私も新人時代は、1日10回以上NullPointerExceptionを出していたような時期がありました。
Optionalの存在を知ってからはその頻度が激減したので、NPEが多い人はOptionalを知らない、というのは実感としてよくわかるあるあるです。
// nullチェックの代わりにOptionalを使う例
Optional<String> name = Optional.ofNullable(map.get("key"));
String result = name.orElse("デフォルト値");
ClassCastException・NumberFormatExceptionの対処法
ClassCastExceptionは、Object型からダウンキャスト(親クラス型の変数を、より具体的な子クラス型に変換すること)する際に実際の型と一致しないと発生します。
instanceofで型を確認してからキャストすれば防げます。
NumberFormatExceptionは文字列を数値に変換する際、数字以外の文字が混ざっていると起きるので、Integer.parseInt()の前に正規表現でバリデーションするか、try-catchで包むのが定石です。
ArrayIndexOutOfBoundsExceptionと回避策
配列やリストの範囲外アクセスは、ArrayIndexOutOfBoundsExceptionの典型例です。
for文のループ条件を「<= 配列長」と書き間違えるだけで簡単に発生します。
ConcurrentModificationExceptionは、拡張for文でループしながらリストの要素を削除したときに起きます。
削除が必要な場面では、Iterator.remove()を使うのが安全です。
IOException・FileNotFoundExceptionの扱い方
ファイル操作系の例外はチェック例外なので、必ず処理を書く必要があります。
FileNotFoundExceptionはパスの指定ミスやファイル未生成が原因になることが大半で、パスの存在確認とパーミッションのチェックで大部分は防げます。
リソースの後始末を忘れずに済むよう、try-with-resources構文を使う習慣をつけておくと安心です。
try-catch-finallyの正しい書き方
例外の種類がわかっても、書き方が雑だとバグが潜在化するだけです。実務で一番多いのが、catchブロックを空のままにしてエラーを握りつぶすパターンです。
catchで例外を握りつぶしてはいけない理由
やってはいけないこと
catchブロックの中身を空にする、いわゆる「例外の握りつぶし」は絶対にやってはいけません。
エラーが起きているのに処理が正常終了したように見えてしまい、原因調査が数倍面倒になります。
最低限、ログにスタックトレースを出力するだけでも、後から追跡できる情報は大きく変わります。
以前、本番環境で@Transactionalが同一クラス内の呼び出しでは効かないことに気づかず、データ不整合が発生してから初めて問題に気づいたことがありました。
エラーを握りつぶすと、気づかないまま進行するバグを量産することになります。
finallyとtry-with-resourcesの使い分け
finallyは例外の有無にかかわらず必ず実行したい処理(接続のクローズなど)に使います。
ただし、ファイルやDB接続のクローズ処理に限っては、Java 7以降のtry-with-resources構文を使う方がシンプルです。
AutoCloseableを実装したリソースなら、finallyでclose()を書かなくても自動的に解放されます。
// try-with-resourcesでクローズ処理を自動化
try (BufferedReader reader = new BufferedReader(new FileReader("data.txt"))) {
String line = reader.readLine();
} catch (IOException e) {
logger.error("ファイル読み込みに失敗しました", e);
}
throws宣言で呼び出し元に例外を伝播させる書き方
throws宣言は、メソッドの中で例外をcatchせず、呼び出し元に処理を委ねるための仕組みです。
IOExceptionのようなチェック例外は、catchせずに済ませたい場合、メソッド定義にthrows IOExceptionと書かないとコンパイルエラーになります。
呼び出し元で例外処理をした方が意味のある場面ではthrows宣言、その場で対処できるならtry-catchと使い分けましょう。
マルチキャッチ構文で複数例外をまとめる
複数の例外を同じ処理でまとめたい場合、catch (IOException | SQLException e)のようにパイプでつなぐマルチキャッチ構文が使えます。
同じcatchブロックを何度も書く必要がなくなり、コードの見通しが良くなります。ただし継承関係にある例外同士は組み合わせられない点には注意しましょう。
独自例外クラスは作るべき?標準例外との判断基準
標準の例外だけで足りるのか、独自の例外クラスを作るべきなのか。この判断で迷う人は多いはずです。
私自身、駆け出しの頃はとりあえずRuntimeExceptionを継承した独自クラスを量産していましたが、後から見返すと情報量が乏しく、結局あまり役に立っていませんでした。
標準例外で足りるケースと自作すべきケース
引数チェックのようなJavaの標準ライブラリでカバーできる範囲は、IllegalArgumentExceptionなど既存クラスで十分です。
独自例外を作るべきなのは、業務ロジック固有のエラー(在庫不足、権限不足など)を呼び出し元に明確に伝えたいときに限ります。
むやみに増やすと、キャッチする側の分岐がかえって複雑になります。
独自例外を作りすぎるのは、以前、将来の拡張に備えてStrategyパターンを導入したものの、結局そのケースが来なかった「過剰設計」の失敗と同じ構造です。
使うかどうかわからない独自例外を先回りして作るのは、YAGNI(You Ain't Gonna Need It)に反する行動でした。
例外メッセージに入れるべき情報
独自例外を作るなら、メッセージには「何が」「どんな値で」失敗したのかを具体的に書きましょう。
「在庫不足です」だけでは調査に時間がかかりますが、「商品ID:1234の在庫が0のため注文できません」まで書けば、ログを見た瞬間に原因がわかります。
原因不明のJavaエラーで詰まったらどうする?チェックリスト
例外の知識だけでは解決しない、環境まわりのつまずきもあります。原因が例外そのものではなく、ログ設定やIDEの使い方に隠れています。
最後に周辺の見直しポイントを確認しておきましょう。
ログ出力設定とスタックトレースの見え方
本番環境ではログレベルをERRORやWARNに絞ることが多く、必要なスタックトレースが出力されていないケースがあります。
logback.xmlやlog4j2.xmlの設定を見直し、少なくとも例外発生時はスタックトレース全体が記録されるようにしておくと、後から原因を追いやすくなります。
IDEのデバッガを使った原因特定
エラーメッセージだけで原因が見えないときは、ブレークポイントを張って変数の中身を1行ずつ確認するのが結局は近道です。
IntelliJ IDEAのEvaluate Expression機能を使えば、実行を止めたまま任意の式を評価できるので、仮説検証のスピードが上がります。
ちなみに今はスタックトレースをそのままClaude Codeに貼るだけで、原因の見当がつくことも増えました。
SES時代に半日かけていた調査が、秒で終わる時代になったのは正直驚きです。
よくある質問(FAQ)
NullPointerExceptionはなぜこんなに多発しますか?
参照型の変数はデフォルトでnullが入る可能性があり、コンパイラがnullチェックを強制しないためです。Java 8以降はOptional型を使うことで、nullの許容範囲を型レベルで明示できます。
例外を握りつぶしても動くなら問題ないですか?
一時的には動きますが、データ不整合や別の場所での二次障害につながる危険があります。最低限、ログには出力しておきましょう。
Errorクラスの例外もcatchしていいですか?
技術的には可能ですが、OutOfMemoryErrorのようなErrorは回復不能な事象を示すため、catchして処理を続けるのは推奨されません。
チェック例外は全部try-catchで囲むべきですか?
いいえ。呼び出し元で処理できないなら、throws宣言で上位に伝播させる選択肢もあります。むやみに握りつぶすより素直なやり方です。
スタックトレースが長すぎて追えないときはどうすればいいですか?
一番上の例外クラス名と、一番下のCaused byだけを先に確認しましょう。途中のフレームワーク部分は読み飛ばして問題ありません。
ConcurrentModificationExceptionはマルチスレッドの問題ですか?
名前に反してシングルスレッドでも発生します。拡張for文でループ中にリストを直接変更すると起きる、実装上の制約が原因です。
まとめ|Exception全体マップを手元に置く
Javaのエラーは、種類と継承関係さえ頭に入っていれば、対処のスピードが劇的に変わります。最後に今回のポイントを整理しておきます。
- Javaのエラーはコンパイルエラー・実行時エラー・論理エラーの3種類に分かれる
- 例外はThrowableからError・Exceptionへ続く継承関係を押さえておく
- スタックトレースは一番上の例外名と一番下のCaused byを先に読む
- catchで握りつぶさず、最低限ログには出力する
- 独自例外は業務ロジック固有のエラーだけに絞って作る
半日かけてログを追っていたSES時代と比べると、今はスタックトレースをそのまま貼るだけで原因の見当がつくことも多く、時代の変化を感じます。
とはいえ、根っこにあるException全体マップの知識がないと、AIの回答を正しく判断することもできません。
この記事が、目の前の赤いエラーと向き合う手助けになれば嬉しいです。