throws と例外の伝播

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

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

  • throws はメソッド宣言に書く「投げる可能性のある例外」のリスト
  • 検査例外 (IOException 等) は throwstry-catch 必須
  • 非検査例外 (RuntimeException 系) は throws に書く必要はない (書いてもOK)
  • 上位メソッドへの「伝播」、catch しない代わりに呼び出し元に投げる

throws と例外の伝播 とは

throws キーワードと検査例外の伝播を学び、Integer.parseInt の失敗を try-catch で受け止めて 0 を返す safeParse メソッドを実装してみよう。

例外は「報告」か「自分で処理」かの 2 択

Java を書いていると、ファイルを読み込んだり、数値に変換したり、ネットワークから値を取ってきたりと、「うまくいくとは限らない処理」を呼ぶ場面が必ず出てきます。Integer.parseInt("42") は成功しますが、Integer.parseInt("xyz") は失敗して NumberFormatException を投げますし、ファイルを開こうとしたら存在しなかった、ということもあります。

このような「呼ぶ側がエラーから逃れられない処理」を扱うとき、Java では呼んだメソッドに対して 2 つの選択肢を迫ります。1 つは try-catch でその場で受け止めて処理すること、もう 1 つは throws で「私はここで処理しない、呼び出し元に任せる」と宣言することです。今回はこの後者、throws による 例外の伝播 (propagation) を中心に学びます。

例外処理は「どこで握るか」を決める設計です。何でもかんでも try-catch するのではなく、適切なレイヤーまで throws で伝播させ、まとめて処理するのがプロの書き方です。Spring や Android の標準的なコードでも、低レイヤーは throws で逃がし、上位のフレームワークが集約して扱う構造になっています。

検査例外と非検査例外の違い

まず押さえておきたいのが、Java の例外には 2 種類あるという事実です。具体的には次のとおりです。

  • 検査例外 (checked exception) — Exception を継承するもの。コンパイラが「try-catch で囲むか throws で宣言するか、どちらかを必ず書け」と強制してくる。代表は IOException SQLException ClassNotFoundException など
  • 非検査例外 (unchecked exception) — RuntimeException を継承するもの。書かなくてもコンパイルが通る。代表は NullPointerException IllegalArgumentException NumberFormatException ArrayIndexOutOfBoundsException など

この違いはとても重要です。たとえば new java.io.FileReader("data.txt") を呼ぶコードは、IOException (検査例外) を投げる可能性があるので、try-catchthrows もないとそもそもコンパイルが通りません。一方、int x = Integer.parseInt(s);NumberFormatException (非検査例外) を投げますが、throws を書かなくてもコンパイルが通ってしまいます。

diagram (will load when visible)

「検査」「非検査」という呼び方は、コンパイラが書き忘れを 検査 してくれるかどうかが由来です。非検査 だから危険、というわけではなく、頻度が高くて毎回書かせると煩雑になるものを RuntimeException の系列にまとめた、というのが言語設計の意図です。

throws キーワードの書き方

throws はメソッド宣言の右側、{ の前に書きます。「このメソッドはこの例外を投げる可能性があるので、呼び出し元は覚悟しておくように」という契約を表します。

Java

import java.io.BufferedReader; import java.io.FileReader; import java.io.IOException; public class FileUtil { // IOException を呼び出し元に伝播する public static String readFirstLine(String path) throws IOException { BufferedReader reader = new BufferedReader(new FileReader(path)); String line = reader.readLine(); reader.close(); return line; } }

このとき大事なのは、readFirstLine を呼ぶ側にも責任が伝染することです。次のように main で呼ぶと、main 側でも try-catchthrows のどちらかが必要になります。

Java

public class Main { public static void main(String[] args) throws IOException { String line = FileUtil.readFirstLine("data.txt"); System.out.println(line); } }

学習中やスクリプト的なコードでは、main メソッドに throws Exception と書いてしまうのが手軽で、これでほぼ全ての検査例外を通せます。本番コードではもう少し丁寧に握る場所を決めるべきですが、勉強用の Solution クラスでは throws Exception で済ませることも多いです。

例外が呼び出し階層を遡る流れ

投げられた例外は、try-catch で受け止めるまで、メソッドの呼び出し階層をどんどん遡っていきます。途中のメソッドが何も書いていなくても、自動的に上に伝わるのが Java の例外の仕組みです。

diagram (will load when visible)

どこか 1 箇所で try-catch すれば、そこから先には例外は伝わりません。逆にどこにも catch がなければ JVM まで届き、スタックトレースが表示されてプログラムが終了します。

例外を握る位置のセオリーは「意味のある対応ができる一番低いレイヤーで握る」です。ライブラリの奥で握って null を返すような実装は、呼び出し元が異常に気づけなくなるので避けるのが鉄則です。

try-catch で受け止めて代替値を返す

今回の課題は、Integer.parseInt という非検査例外を投げる可能性のあるメソッドを呼び、失敗した場合は 0 を返すという仕様です。NumberFormatException は非検査例外なので throws を書かなくてもコンパイルは通りますが、握らないと例外がそのまま上に伝わってしまうので try-catch で受け止めます。

Java

public class Solution { public static int safeParse(String s) { try { return Integer.parseInt(s); } catch (NumberFormatException e) { return 0; } } }

try の中で Integer.parseInt(s) を呼び、成功すればその値が return されます。失敗すると NumberFormatException が投げられ、catch ブロックに制御が移って 0 が返ります。null を渡したときは Integer.parseInt(null)NumberFormatException("Cannot parse null string")を投げるので、今回の catch (NumberFormatException e) のままでも 0 が返ります。今回のテストでは null 対応までは求めません。

よくある間違い

例外まわりは初学者がつまずきやすい場所です。よくあるミスを 3 つ紹介します。

  • throws を書き忘れて検査例外を投げるFileReader などを使うコードを書いて throws IOException を忘れると、unreported exception IOException; must be caught or declared to be thrown というエラーが出ます。コンパイラのメッセージのとおり、try-catchthrows のどちらかを必ず書きましょう
  • catch (Exception e) {} で握り潰す — 例外を catch しただけで中身を空にすると、エラーが起きてもログにも出ず、原因の追跡が極めて困難になります。最低でも e.printStackTrace() を呼ぶか、throw new RuntimeException(e) のように包み直して再送出するのが鉄則です
  • 非検査例外に throws を書くthrows NullPointerException のように非検査例外を throws 句に書いても、コンパイラは強制してくれませんし、可読性も上がりません。Javadoc@throws タグでドキュメントに書く方が読みやすいです

catch ブロックを空っぽにすると、未来の自分や同僚を確実に泣かせます。チームで一番嫌われる書き方の 1 つなので、// 意図的に無視 というコメントすら付けずに空 catch を残すのは絶対にやめましょう。

もう 1 つ細かい注意として、catch の中で return した値は、tryreturn を上書きします。今回のコードで tryreturn Integer.parseInt(s) が成功すればその値が返り、失敗したら catchreturn 0 が返るのはこのためです。

実務で遭遇するパターン

中級レッスンで学んだことが、実際の業務コードでどう登場するかを整理します。

  • Repository → Service → Controller の伝播 ─ DB アクセスの SQLException を上位レイヤーへ
  • ライブラリ API の設計 ─ ファイル / ネット系メソッドの throws IOException
  • throws Exception の禁じ手 ─ 広すぎて呼び出し元が困る、業務固有例外に変換するべき
  • メインメソッドの throws ─ サンプルコードなら public static void main(...) throws Exception で済ますことが多い

コードレビューで指摘されがちなポイント

PR を出すとシニアから入りやすい指摘です。先回りで身につけておけばレビューが一発で通ります。

  • throws Exception で広く宣言 ─ 何が来るか分からないので呼び出し元が catch しづらい
  • throws 多すぎ ─ 5 個以上並ぶと業務固有例外で 1 つにまとめる検討
  • catch するべきところで throws ─ ローカルで対処可能なエラーは catch する
  • インタフェースの実装側で throws を増やす ─ オーバーライドルール違反、コンパイルエラー

パフォーマンス考慮事項

  • throws 自体はコストゼロ ─ コンパイル時の宣言、実行時のオーバーヘッドなし
  • 例外を実際に投げると重いthrows の宣言ではなく throw の実行が重い
  • stacktrace の自動生成 ─ 例外 instance 生成時に発生、コンストラクタで super(msg, cause, false, false) で抑制可
  • Wrapping のコストIOException を業務例外で包むのは別 instance 生成、ホットパスでは少し効く
この章のポイント

ここまでの要点 throws ExceptionType でメソッドが投げる検査例外を宣言。Exception を広く投げず、業務固有例外で意味を持たせる。

やってみよう

それでは本題の課題に挑戦しましょう。Solution.safeParse(String s) を実装します。やることは次のとおりです。

  1. try ブロックの中で Integer.parseInt(s) を呼び、結果をそのまま return する
  2. catch (NumberFormatException e) ブロックで 0return する
  3. メソッドの戻り値の型は int、引数は String s のままにする

テストでは safeParse("42")42 が返り、safeParse("xyz")0 が返るかを確認します。Integer.parseInt は前後に空白があると失敗するので、もし将来 " 42 " のような入力も通したいなら s.trim() してから渡すと良いでしょう。今回のテストはきれいな数字文字列か、明らかに数字でない文字列だけを渡しますので、trim は無くて構いません。

慣れてきたら、Integer.parseIntLong.parseLong に変えてオーバーフロー対策を考えてみたりすると、例外設計の感覚がさらに身につきます。

次のレッスンでは、独自例外 (extends RuntimeException した自作クラス) を定義して、ビジネスロジックのエラーに名前を付ける方法を学びます。まずは標準例外を try-catchthrows で扱う感覚を、この safeParse でしっかりつかんでおきましょう。

よくある質問

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 が第一選択です。

次のレッスン

次は 独自の例外を作る で、独自の例外を作る を学びます。

事前確認 — 進む前に次の 3 つができることを確認しましょう。

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

理解度チェック (30 秒)

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

この章のポイント

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

関連レッスン

要件

  1. try ブロックで Integer.parseInt(s) を呼び、成功時はその戻り値を return すること
  2. catch (NumberFormatException e) ブロックで 0return すること
  3. メソッドシグネチャは public static int safeParse(String s) のままにし、戻り値の型は int を保つこと

入出力例

test-cases.txt

safeParse("42")42 safeParse("xyz")0 safeParse("-7")-7 safeParse("")0 safeParse("0")0

ヒント

main.java
main.java
学習モード

メモ

throws と例外の伝播

⌘S で保存