throws と例外の伝播
このレッスンで分かること
throwsはメソッド宣言に書く「投げる可能性のある例外」のリスト- 検査例外 (
IOException等) はthrowsかtry-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で宣言するか、どちらかを必ず書け」と強制してくる。代表はIOExceptionSQLExceptionClassNotFoundExceptionなど非検査例外(unchecked exception) —RuntimeExceptionを継承するもの。書かなくてもコンパイルが通る。代表はNullPointerExceptionIllegalArgumentExceptionNumberFormatExceptionArrayIndexOutOfBoundsExceptionなど
この違いはとても重要です。たとえば new java.io.FileReader("data.txt") を呼ぶコードは、IOException (検査例外) を投げる可能性があるので、try-catch も throws もないとそもそもコンパイルが通りません。一方、int x = Integer.parseInt(s); は NumberFormatException (非検査例外) を投げますが、throws を書かなくてもコンパイルが通ってしまいます。
「検査」「非検査」という呼び方は、コンパイラが書き忘れを
検査してくれるかどうかが由来です。非検査だから危険、というわけではなく、頻度が高くて毎回書かせると煩雑になるものを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-catch か throws のどちらかが必要になります。
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 の例外の仕組みです。
どこか 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-catchかthrowsのどちらかを必ず書きましょうcatch (Exception e) {}で握り潰す — 例外をcatchしただけで中身を空にすると、エラーが起きてもログにも出ず、原因の追跡が極めて困難になります。最低でもe.printStackTrace()を呼ぶか、throw new RuntimeException(e)のように包み直して再送出するのが鉄則です- 非検査例外に
throwsを書く —throws NullPointerExceptionのように非検査例外をthrows句に書いても、コンパイラは強制してくれませんし、可読性も上がりません。Javadocの@throwsタグでドキュメントに書く方が読みやすいです
catchブロックを空っぽにすると、未来の自分や同僚を確実に泣かせます。チームで一番嫌われる書き方の 1 つなので、// 意図的に無視というコメントすら付けずに空 catch を残すのは絶対にやめましょう。
もう 1 つ細かい注意として、catch の中で return した値は、try の return を上書きします。今回のコードで try の return Integer.parseInt(s) が成功すればその値が返り、失敗したら catch の return 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) を実装します。やることは次のとおりです。
tryブロックの中でInteger.parseInt(s)を呼び、結果をそのままreturnするcatch (NumberFormatException e)ブロックで0をreturnする- メソッドの戻り値の型は
int、引数はString sのままにする
テストでは safeParse("42") で 42 が返り、safeParse("xyz") で 0 が返るかを確認します。Integer.parseInt は前後に空白があると失敗するので、もし将来 " 42 " のような入力も通したいなら s.trim() してから渡すと良いでしょう。今回のテストはきれいな数字文字列か、明らかに数字でない文字列だけを渡しますので、trim は無くて構いません。
慣れてきたら、Integer.parseInt を Long.parseLong に変えてオーバーフロー対策を考えてみたりすると、例外設計の感覚がさらに身につきます。
次のレッスンでは、独自例外 (extends RuntimeException した自作クラス) を定義して、ビジネスロジックのエラーに名前を付ける方法を学びます。まずは標準例外を try-catch と throws で扱う感覚を、この 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 つができることを確認しましょう。
- throws の要点を自分の言葉で説明できる
- このレッスンの最小コード (または操作手順) を見ずに書ける
- 練習問題やクイズで間違えた箇所を読み直して理解した
理解度チェック (30 秒)
Q. throws とは何か、1 文で説明してください。
A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。
関連レッスン
要件
tryブロックでInteger.parseInt(s)を呼び、成功時はその戻り値をreturnすることcatch (NumberFormatException e)ブロックで0をreturnすること- メソッドシグネチャは
public static int safeParse(String s)のままにし、戻り値の型はintを保つこと
入出力例
test-cases.txt
safeParse("42") → 42
safeParse("xyz") → 0
safeParse("-7") → -7
safeParse("") → 0
safeParse("0") → 0