throws の全体像
throws は「このメソッドは特定の例外を投げる可能性がある」と呼び出し側へ宣言するための構文です。主にチェック例外(IOException など)に使われ、呼び出し側に「捕まえるか、さらに投げるか」を選ばせます。設計の要点は「この層で回復しないなら、原因情報を保ったまま上位へ伝える」ことです。
基本構文と動き
メソッド宣言での throws
import java.io.*;
public class Reader {
public static String firstLine(String path) throws IOException {
try (var br = new BufferedReader(new FileReader(path))) {
return br.readLine();
}
}
}
Javaこの firstLine は入出力に失敗すると IOException を投げる可能性がある、と宣言しています。呼び出し側は try-catch で処理するか、さらに throws IOException を付けて上位へ伝播させます。
複数の例外を宣言
public static int loadPort(String env) throws NumberFormatException, IllegalStateException {
String s = System.getenv(env);
if (s == null) throw new IllegalStateException("環境変数がありません: " + env);
return Integer.parseInt(s); // 不正なら NumberFormatException
}
Java複数書けますが、実行時例外(RuntimeException系)は宣言しなくても構いません(宣言してもよい)。チェック例外は宣言か捕捉が必須です。
チェック例外と実行時例外(重要ポイントの深掘り)
どちらを throws するかの指針
チェック例外は「呼び出し側が現実的に回復できる」外部要因向け(I/O、ネットワーク、SQL)。この層で回復しない設計なら、具体例外をそのまま(または意味のある型へ包んで)throws で伝えます。実行時例外は契約違反やバグ向けで、通常は事前検証で防ぎ、throws 宣言は不要です。
層ごとの変換(例外の意味を合わせる)
インフラ層で SQLException が発生したら、アプリ層へは IllegalStateException やドメイン固有の例外に包み直すと、層ごとの責務が明確になります。包む際は原因例外(cause)を必ず渡し、スタックトレースを保ちます。
try {
dao.save(order);
} catch (java.sql.SQLException e) {
throw new IllegalStateException("注文保存に失敗: id=" + order.id(), e);
}
Java伝播設計とエラーハンドリングの分担
「この層で回復しない」なら上位へ委ねる
アプリのトップレベル(UI層・HTTPハンドラなど)で一括ハンドリングすると、ログやユーザー通知が整理されます。中間層は「意味を付ける」「補足情報を足す」以外は無理に回復しないのが読みやすく、保守しやすい設計になります。
API の契約としての throws
throws はメソッド契約の一部です。「いつ・なぜ失敗するか」を JavaDoc に明記し、呼び出し側が正しく対処できるようにします。
/**
* ファイルの先頭行を返す。
* @param path 読み込むファイルのパス
* @return 先頭行(null の可能性あり)
* @throws IOException ファイルがない・読み込みに失敗した場合
*/
public static String firstLine(java.nio.file.Path path) throws java.io.IOException { ... }
Javatry-with-resources と throws の相性
try-with-resources は後始末(close)を自動化し、本体例外をそのまま throws できます。クローズ時の例外は「サプレスト例外」として本体に付属し、呼び出し側で getSuppressed() を確認できます。これにより「情報を失わず、適切な層で処理」がしやすくなります。
import java.nio.file.*;
import java.io.*;
static String read(Path p) throws IOException {
try (var br = Files.newBufferedReader(p)) {
return br.readLine();
}
}
Javaよくある落とし穴(重要ポイントの深掘り)
throws で投げっぱなしにして握り潰す
上位で何もしないと「静かに失敗」します。トップレベルに到達した例外は、ログ・ユーザー通知・フォールバック(必要なら)を設計してください。「どこで責任を持つか」をチームで明文化するのが重要です。
catch で原因を捨てる包み直し
原因例外を渡さないと、根本原因が追えません。必ず new Xxx("msg", e) の形で原因を連鎖させます。
広すぎる throws 宣言
「throws Exception」は呼び出し側の負担を過大にします。実際に起こり得る型だけを宣言し、意味のある抽象へ束ねたい場合は自作の例外型(アプリ層の共通例外)へ包みます。
チェック例外を無理に実行時例外へ
安易な実行時例外への変換は「回復可能性」を失わせます。ユーザー入力や環境依存の失敗は、呼び出し側が選択できるようチェック例外を維持するか、結果オブジェクト(成功/失敗)で返す設計も検討します。
例題で身につける
例 1: 読み込みはこの層で回復せず、上位へ委ねる
import java.nio.file.*;
import java.io.*;
public class Loader {
public static String firstLine(Path path) throws IOException {
try (var br = Files.newBufferedReader(path)) {
return br.readLine();
}
}
public static void main(String[] args) {
try {
System.out.println(firstLine(Path.of("input.txt")));
} catch (IOException e) {
System.err.println("読込失敗: path=" + pathSafe(e));
e.printStackTrace(); // 集約ハンドリング
}
}
static String pathSafe(Exception e) { return e.getMessage(); }
}
Java例 2: 低層の具体例外を意味のある型へ包んで再スロー
class Repository {
User find(String id) {
try {
// DBアクセス...
return new User(id, "Sato");
} catch (java.sql.SQLException e) {
throw new IllegalStateException("DB失敗: id=" + id, e); // 原因を連鎖
}
}
}
Java例 3: 入力検証は事前に、異常は throws で伝える
public class Price {
public static int taxed(int subtotal, double taxRate) {
if (subtotal < 0) throw new IllegalArgumentException("subtotal must be >= 0");
if (taxRate < 0 || taxRate > 1) throw new IllegalArgumentException("taxRate must be 0..1");
return (int) Math.round(subtotal * (1 + taxRate));
}
}
Java契約違反は実行時例外で早期に弾き、外部要因の失敗はチェック例外として throws で上位へ。
仕上げのアドバイス(重要部分のまとめ)
throws は「このメソッドで回復しない失敗を、原因情報を保って上位へ渡す」宣言です。チェック例外は回復可能性のために維持し、実行時例外は契約違反の早期検出に使う。層に応じて意味のある型へ包み直し、原因を連鎖させる。トップレベルでの集約ハンドリングを設計し、try-with-resourcesで後始末を自動化して情報を失わない——この型が身につくと、例外設計はシンプルで強くなります。
