try-catch で例外をつかむ

生田 陸人
LuaGate エンジニア / 現役エンジニア
編集 LuaGate編集部

このレッスンで分かること

  • try に「失敗するかもしれない処理」、catch に「失敗時の後始末」を書く
  • 代表的な例外は NullPointerException / NumberFormatException / ArithmeticException / ArrayIndexOutOfBoundsException
  • 例外を握りつぶさず、最低でも原因を logstacktrace に残す
  • 最小例は 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" の長い赤いログ、というのが典型的な見え方です。例外処理を学ぶというのは、こうした強制終了を回避し、想定外でもアプリを動かし続けるための「安全網」を張る技術を学ぶことに他なりません。

diagram (will load when visible)

この仕組みを使う基本構文が 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 の子孫で、書き方を間違えると簡単に発生します。

  • NullPointerExceptionnull の参照先のメソッド / フィールドにアクセスしたとき。String s = null; s.length(); のような書き方で発生
  • NumberFormatExceptionInteger.parseInt("abc") のように、数値に変換できない文字列を渡したとき
  • ArithmeticException ─ 整数の 0 除算 10 / 0 のとき。double の 0 除算は Infinity になるのでこの例外にはならない
  • ArrayIndexOutOfBoundsException ─ 配列で存在しないインデックスを指定したとき。String.charAt の範囲外アクセスは StringIndexOutOfBoundsException が発生する(どちらも IndexOutOfBoundsException のサブクラス)
diagram (will load when visible)

Error 系 (OutOfMemoryError StackOverflowError など) は JVM 自体の重大な異常を表すので、基本的にアプリ側でキャッチしません。アプリで扱うのは Exception から下の枝、特に RuntimeException 系がメインだ、と覚えておくと整理しやすいです。

Exception には大きく分けて 検査例外 (checked exception) と 非検査例外 (unchecked exception) があります。IOException のような検査例外は try-catchthrows 宣言で必ず処理しないとコンパイルエラーになります。一方、上に挙げた NumberFormatException などの RuntimeException 系は非検査例外で、書かなくてもコンパイルは通りますが、本番では油断していると突然落ちます。

catch ブロックでの選択肢 ─ ログ / デフォルト値 / 再 throw

catch でつかまえた例外を、どう処理するかは設計判断です。代表的な選択肢は次の 3 つです。

  1. ログに残すe.printStackTrace();logger.error("failed", e); のように、開発者が後で原因を追えるよう記録する。サーバーアプリでは必須
  2. デフォルト値を返す ─ 数値変換に失敗したら 0 を返す、設定読み込みに失敗したら空文字を返す、など、アプリ全体は止めずに「安全な代わりの値」で続行する
  3. 別の例外として投げ直す (再 throw) ─ 低レベルの IOException をキャッチして、業務的に意味のある UserNotFoundException に変換して投げる、といったパターン。原因の enew 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=2parseInt するとき、?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 に後始末。型はピンポイントで指定し、握りつぶさずに logstacktrace を残す。正常系のフロー制御には使わない。

やってみよう

それでは今回の課題です。Solution.parseOrZero(String s) メソッドを完成させてください。やることは次のとおりです。

  1. メソッドの中に try ブロックを書き、その中で Integer.parseInt(s) を呼ぶ
  2. parseInt の戻り値をそのまま return する
  3. catch (NumberFormatException e) で例外をつかむ
  4. 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 つができることを確認しましょう。

  1. try-catch の要点を自分の言葉で説明できる
  2. このレッスンの最小コード (または操作手順) を見ずに書ける
  3. 練習問題やクイズで間違えた箇所を読み直して理解した

理解度チェック (30 秒)

Q. try-catch とは何か、1 文で説明してください。

この章のポイント

A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。

関連レッスン

要件

  1. try ブロックの中で Integer.parseInt(s) を呼び、その戻り値を return すること
  2. catch (NumberFormatException e) で例外を捕まえ、catch ブロックの中で 0 を返すこと
  3. 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

ヒント

main.java
main.java
学習モード

メモ

try-catch で例外をつかむ

⌘S で保存