try-catch で例外をつかむ
このレッスンで分かること
tryに「失敗するかもしれない処理」、catchに「失敗時の後始末」を書く- 代表的な例外は
NullPointerException/NumberFormatException/ArithmeticException/ArrayIndexOutOfBoundsException- 例外を握りつぶさず、最低でも原因を
logかstacktraceに残す- 最小例は
try { return Integer.parseInt(s); } catch (NumberFormatException e) { return 0; }
try-catch で例外をつかむ とは
Java の例外処理の基本となる try-catch 構文を学び、NumberFormatException を捕まえてデフォルト値を返すコードを書いてみよう。
「想定外」とどう付き合うか ─ 例外という仕組み
プログラムを書いていると、書いた本人すら予想していなかった入力やデータがやってくる場面が必ずあります。たとえば「年齢を整数で入力してください」と言っているのに abc という文字列が飛んできたり、API から返ってくるはずの数字が空文字 "" だったり、0 で割り算をしようとしたり。こうした「ふつうの処理ルートからは外れた事態」を、Java では 例外 (Exception) という名前のオブジェクトで表します。
例外は「エラーメッセージ」のことではなく、
Throwableを継承した正式な Java オブジェクトです。中にはどこで何が起きたかというstacktrace(スタックトレース) やmessage(メッセージ) が詰まっていて、catchブロックで受け取って中身を調べられます。
例外を一切処理しないと、Java は「もう続きを実行できない」と判断してプログラムをそこで強制終了します。Android アプリならクラッシュ、サーバーアプリなら 500 エラーの応答、コマンドラインなら Exception in thread "main" の長い赤いログ、というのが典型的な見え方です。例外処理を学ぶというのは、こうした強制終了を回避し、想定外でもアプリを動かし続けるための「安全網」を張る技術を学ぶことに他なりません。
この仕組みを使う基本構文が try-catch です。try の中に「失敗するかもしれない処理」を書き、catch で「失敗したときの後始末」を書く、と覚えておきましょう。
try-catch の基本構文
最小構成は次のとおりです。try ブロックに本来やりたい処理、catch ブロックに例外発生時の処理を書きます。
Java
try {
// 例外が起きるかもしれない処理
int n = Integer.parseInt("42");
System.out.println(n);
} catch (NumberFormatException e) {
// 例外が起きたときの処理
System.out.println("数値に変換できませんでした");
}注目したいのは catch のかっこの中身です。catch (NumberFormatException e) という書き方は、「NumberFormatException 型の例外を e という名前で受け取る」という意味になります。e の中には getMessage() や printStackTrace() といったメソッドがあり、原因の文字列や発生箇所を取り出せます。
try-catchは「最初にtry、その直後にcatch」が文法的なセットです。catchを書かずにtryだけを書くと、finallyを併用しない限りコンパイルエラーになります。逆にfinallyブロック (後始末専用) を付けてtry-finallyという形にすることはできます。
try ブロックの中で例外が起きると、その瞬間に処理が中断され、対応する catch にジャンプします。try 内の残りの行はスキップされる、というのが重要なポイントです。たとえば次のコードで int n = Integer.parseInt("abc"); が失敗すると、その直後の System.out.println(n); は 絶対に実行されません。プログラムは即座に catch 側へ飛んでいきます。
代表的な例外クラスたち
Java には大量の例外クラスがありますが、入門〜中級で押さえておきたい代表選手は次の 4 つです。どれも RuntimeException の子孫で、書き方を間違えると簡単に発生します。
NullPointerException─nullの参照先のメソッド / フィールドにアクセスしたとき。String s = null; s.length();のような書き方で発生NumberFormatException─Integer.parseInt("abc")のように、数値に変換できない文字列を渡したときArithmeticException─ 整数の 0 除算10 / 0のとき。doubleの 0 除算はInfinityになるのでこの例外にはならないArrayIndexOutOfBoundsException─ 配列で存在しないインデックスを指定したとき。String.charAtの範囲外アクセスはStringIndexOutOfBoundsExceptionが発生する(どちらもIndexOutOfBoundsExceptionのサブクラス)
Error 系 (OutOfMemoryError StackOverflowError など) は JVM 自体の重大な異常を表すので、基本的にアプリ側でキャッチしません。アプリで扱うのは Exception から下の枝、特に RuntimeException 系がメインだ、と覚えておくと整理しやすいです。
Exceptionには大きく分けて検査例外(checked exception) と非検査例外(unchecked exception) があります。IOExceptionのような検査例外はtry-catchかthrows宣言で必ず処理しないとコンパイルエラーになります。一方、上に挙げたNumberFormatExceptionなどのRuntimeException系は非検査例外で、書かなくてもコンパイルは通りますが、本番では油断していると突然落ちます。
catch ブロックでの選択肢 ─ ログ / デフォルト値 / 再 throw
catch でつかまえた例外を、どう処理するかは設計判断です。代表的な選択肢は次の 3 つです。
- ログに残す ─
e.printStackTrace();やlogger.error("failed", e);のように、開発者が後で原因を追えるよう記録する。サーバーアプリでは必須 - デフォルト値を返す ─ 数値変換に失敗したら
0を返す、設定読み込みに失敗したら空文字を返す、など、アプリ全体は止めずに「安全な代わりの値」で続行する - 別の例外として投げ直す (
再 throw) ─ 低レベルのIOExceptionをキャッチして、業務的に意味のあるUserNotFoundExceptionに変換して投げる、といったパターン。原因のeをnew MyException("...", e)のように渡しておくと、元のトレースも保たれる
今回の課題は 2 番目の「デフォルト値を返す」パターンです。parseOrZero("abc") のように変換できない文字列が来たら、例外で落とすのではなく 0 を返して呼び出し元が処理を続けられるようにします。Web フォームのバリデーションや、CSV の読み込みなどで頻繁に出てくる定番パターンなので、しっかり身につけてください。
Java
public static int parseOrZero(String s) {
try {
return Integer.parseInt(s);
} catch (NumberFormatException e) {
return 0;
}
}try ブロック内で return が成功すれば、その値がそのまま呼び出し元に返ります。失敗して catch に飛んだ場合は、catch ブロックの return 0; が実行されて 0 が返る、という流れです。
よくある間違い
例外処理は便利な反面、雑に書くと逆に「原因不明のバグの温床」になります。初学者がやらかしがちなアンチパターンを 3 つ紹介します。
catch (Exception e)で握りつぶす ─ 何でもキャッチしてcatchブロックが空っぽ ({}だけ) になっているコード。例外が完全に消えてしまい、本番でバグが起きても原因を追えません。最低でもe.printStackTrace();か業務ログ出力を入れる、というのが鉄則ですstacktraceを捨てる ─catch (Exception e) { throw new RuntimeException("失敗"); }のように、元のeを渡さずに新しい例外を作るパターン。これだと「失敗」とだけ書かれた例外が飛んで、本当の原因 (どのファイルの何行目で何が起きたか) が消えます。必ずthrow new RuntimeException("失敗", e);のように原因を引き継ぎましょうcatchの順序を間違える ─ 複数のcatchを並べるときは、catch (Exception e)のような広いものを後ろに書きます。先頭に書くと、その後ろのcatch (NumberFormatException e)は「絶対に到達できないコード」とみなされてコンパイルエラーになります。狭い型から広い型へ、の順番が原則です
もう 1 つ、テスト課題でやりがちなのが catch (Exception e) と書いてしまうことです。広い型でも動きますが、今回の課題のように「特定の例外だけ拾いたい」場面では、catch (NumberFormatException e) のようにピンポイントで指定するのが現代 Java の作法です。広い型でつかむと、本来通知すべき別のバグまで隠してしまう可能性があります。
例外を握りつぶす書き方は、初学者が「とりあえずエラーが消えればいい」とやりがちなパターンです。短期的には動いて見えても、半年後に本番でデータ不整合が出たときに地獄を見ます。例外が起きたら、最低でも何が起きたかをログに残す、を絶対のルールにしましょう。
実務で遭遇するパターン
中級レッスンで学んだことが、実際の業務コードでどう登場するかを整理します。
- 外部入力の数値変換 ─ HTTP リクエストの
?page=2をparseIntするとき、?page=abcの攻撃に備える - 設定ファイルの読み込み ─ 設定値が空でも止めずにデフォルトで動かす
- CSV / Excel のバッチ取り込み ─ 1 行の不正で全体を止めず、エラー行だけ別ファイルに退避
- マイクロサービス間の HTTP 呼び出し ─ 一時的な
IOExceptionをキャッチしてリトライ / サーキットブレーカーへ
コードレビューで指摘されがちなポイント
PR を出すとシニアから入りやすい指摘です。先回りで身につけておけばレビューが一発で通ります。
catch (Exception e)で広く受けるのは原則 NG ─ 想定外のRuntimeExceptionまで握りつぶすcatchブロックが空 (例外を飲み込む) ─ 最低でもlog.warn("...", e)で残すe.printStackTrace()を本番に残す ─ ローカルでは便利だが本番はlogger.error("...", e)に置き換える- 業務固有の例外を作らない ─
SQLExceptionをそのまま上位に投げ続けると Controller で意味が読めない
パフォーマンス考慮事項
try-catch自体は正常系で軽い ─ JIT が最適化するため測定可能なオーバーヘッドはほぼゼロ- 例外を投げる側は重い ─
new Exception()はstacktrace生成で 1 回数 μs〜数十 μs - フロー制御に例外を使わない ─
map.containsKeyのチェックを try-catch でやると 10〜100 倍遅い stacktraceを抑制する選択肢 ─ 大量に出る予期例外はe.getMessage()だけ残す実務テクニックがある
ここまでの要点
try に失敗するかもしれない処理、catch に後始末。型はピンポイントで指定し、握りつぶさずに log か stacktrace を残す。正常系のフロー制御には使わない。
やってみよう
それでは今回の課題です。Solution.parseOrZero(String s) メソッドを完成させてください。やることは次のとおりです。
- メソッドの中に
tryブロックを書き、その中でInteger.parseInt(s)を呼ぶ parseIntの戻り値をそのままreturnするcatch (NumberFormatException e)で例外をつかむcatchブロックでreturn 0;を書いて 0 を返す
具体的な期待動作は次のとおりです。
parseOrZero("42")は42を返すparseOrZero("abc")は変換に失敗するので0を返すparseOrZero("")も空文字は数値にできないので0を返すparseOrZero("-5")はマイナス記号付きでもパースできるので-5を返す
テストでは戻り値の int を直接比較します。String で "42" のように返してしまうと型が合わずに fail するので、必ず int を返してください。Integer.parseInt の戻り値はすでに int 型なので、そのまま return すれば OK です。
余裕があれば、catch の中で System.out.println(e.getMessage()); を入れて、NumberFormatException のメッセージがどんな形なのか覗いてみてください。For input string: "abc" のような文字列が見えるはずです。実物を一度見ておくと、本番でログから原因を追うときの感覚がぐっと身につきます。次のレッスンでは finally ブロックや、複数の catch を組み合わせるパターンに踏み込んでいきます。
よくある質問
Q. 例外を catch する単位はどれくらいが適切ですか?
A. 意味のあるリカバリができる範囲ごとに細かく catch するのが基本です。広く catch (Exception e) で全部受けると本当の原因が見えなくなります。具体的な例外型(IOException, SQLException)で個別対応し、最後に総合受けを置くと安全と網羅の両立ができます。
Q. checked と unchecked の使い分けは?
A. 回復可能な業務エラーは checked(IOException 系)、プログラマのバグは unchecked(RuntimeException 系)が基本ですが、現代の API(Spring、Stream)では unchecked 寄りが主流です。throws 宣言の煩雑さを避けたいなら unchecked を選びます。
Q. finally で何を書くべきですか?
A. リソース解放(ファイルクローズ、DB 接続クローズ)が定番です。ただし AutoCloseable を実装したクラスなら try-with-resources を使うと finally を書かずに自動クローズできるため、現代の Java では try-with-resources が第一選択です。
次のレッスン
次は 複数の catch ブロック で、複数の catch ブロック を学びます。
事前確認 — 進む前に次の 3 つができることを確認しましょう。
- try-catch の要点を自分の言葉で説明できる
- このレッスンの最小コード (または操作手順) を見ずに書ける
- 練習問題やクイズで間違えた箇所を読み直して理解した
理解度チェック (30 秒)
Q. try-catch とは何か、1 文で説明してください。
A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。
関連レッスン
要件
tryブロックの中でInteger.parseInt(s)を呼び、その戻り値をreturnすることcatch (NumberFormatException e)で例外を捕まえ、catchブロックの中で0を返すことcatch (Exception e)のような広すぎる型ではなく、NumberFormatExceptionをピンポイントで指定すること
入出力例
test-cases.txt
parseOrZero("42") → 42
parseOrZero("abc") → 0
parseOrZero("") → 0
parseOrZero("-5") → -5
parseOrZero("100") → 100
parseOrZero("3.14") → 0