この記事の要点
• throws はメソッドが例外を投げうることを宣言する
• throw は実際に例外オブジェクトを投げる
• 独自例外クラスを作ることで業務ロジックのエラーを型で表現できる
このシリーズについて
「Java入門」シリーズの第20回です。前回 #19 - 例外処理の基本 で try-catch を学びました。本記事では例外を呼び出し元に委譲する仕組みと、自分で例外を投げる方法を解説します。
前提条件: #19 - 例外処理の基本 で try-catch と checked / unchecked 例外の概念を理解していること。
throws とは
メソッドの中で checked 例外が発生する可能性があるとき、次の2つの選択肢があります。
- そのメソッド内で try-catch する
- メソッド宣言に
throwsを付けて、呼び出し元に処理を任せる
// Java 21 - メソッド内で try-catch する例
public void readFileA(String path) {
try {
FileReader reader = new FileReader(path); // IOException (checked)
// 読み込み処理
} catch (IOException e) {
System.err.println("ファイルが開けませんでした: " + e.getMessage());
}
}
// Java 21 - throws で呼び出し元に委譲する例
import java.io.FileReader;
import java.io.IOException;
public void readFileB(String path) throws IOException {
FileReader reader = new FileReader(path); // try-catch 不要
// 読み込み処理
}
2つ目の書き方では、readFileB を呼び出す側が try-catch するか、さらに上位に throws IOException を付けて連鎖させます。
ポイント: checked 例外をそのメソッドで回復できない場合、throws で上位に任せる方が自然です。最終的には main メソッドや上位レイヤで統一的にハンドリングします。
throw キーワード:例外を投げる
自分でエラー条件を判定して例外を投げるには throw を使います。
// Java 21 - 負数が渡されたら例外を投げる
public void setAge(int age) {
if (age < 0) {
throw new IllegalArgumentException("年齢は0以上でなければなりません: " + age);
}
this.age = age;
}
throw の後ろには例外オブジェクト(new XXXException(...))を書きます。IllegalArgumentException は unchecked 例外なので、throws 宣言は不要です。
| キーワード | 役割 |
|---|---|
throw | 実際に例外オブジェクトを投げる(処理中断) |
throws | メソッド宣言に付け、例外が投げられる可能性を示す |
注意: throw は**s なし**、throws は**s あり**です。綴りミスでコンパイルエラーになりやすいので注意しましょう。
throws / throw / try-catch の役割対比
| 構文 | 使う場所 | 役割 |
|---|---|---|
throws XxxException | メソッド宣言の末尾 | このメソッドが例外を投げうることを宣言(checked 例外は必須) |
throw new XxxException() | メソッド本体内 | 実際に例外オブジェクトを生成して投げる |
try-catch | メソッド本体内 | 例外を捕捉して処理する(上位に伝播させない) |
checked 例外 vs unchecked 例外の使い分け
前回の復習も兼ねて、実務での使い分け方針を整理します。
| 種類 | 継承元 | throws 宣言 | 使うべき場面 | 代表例 |
|---|---|---|---|---|
| Checked Exception | Exception だが RuntimeException 以外 | 必須 | 外部要因で予期可能なエラー(ファイルなし、ネットワーク切断) | IOException, SQLException |
| Unchecked Exception | RuntimeException | 任意(普通は書かない) | プログラムのバグ、引数の前提条件違反 | NullPointerException, IllegalArgumentException |
checked 例外は「こうなることがありうるから呼び出し側で対処してね」という設計意図を示します。unchecked 例外は「呼び出し方が間違っている」「ここに到達すること自体がバグ」という状況で投げます。
ポイント: 業務ロジックのエラー(「在庫が足りない」「権限がない」)を checked 例外にするか unchecked にするかは議論がありますが、近年は unchecked(RuntimeException 継承)にして、必要な箇所だけ try-catch する設計が好まれます。
自作例外クラス
Java 標準の例外だけでなく、独自の例外クラスを作ることができます。
// Java 21 - 独自の checked 例外
public class InsufficientStockException extends Exception {
public InsufficientStockException(String message) {
super(message);
}
}
// Java 21 - 独自の unchecked 例外
public class InvalidUserIdException extends RuntimeException {
public InvalidUserIdException(String message) {
super(message);
}
}
継承元を決める基準:
| 継承元 | いつ使うか |
|---|---|
Exception | 呼び出し元に必ず try-catch または throws を強制したい |
RuntimeException | 呼び出し元が任意にキャッチできれば良い |
実務では RuntimeException を継承して、ドメインロジックの異常(OrderNotFoundException、PaymentFailedException 等)を表現するケースが増えています。
ヒント: 例外クラス名は必ず Exception で終わらせる慣例があります(例: UserNotFoundException)。これにより、コードを読んだときに例外だとすぐわかります。
実行例1:throws を使ったメソッド連鎖
// Java 21 - ファイル読み込みロジックを分離する例
import java.io.FileReader;
import java.io.BufferedReader;
import java.io.IOException;
public class FileService {
// throws で上位に委譲
public String readFirstLine(String path) throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
return br.readLine();
}
}
public static void main(String[] args) {
FileService service = new FileService();
try {
String line = service.readFirstLine("data.txt");
System.out.println("最初の行: " + line);
} catch (IOException e) {
System.err.println("ファイル読み込み失敗: " + e.getMessage());
}
}
}
readFirstLine は throws IOException を宣言しているため、呼び出し元(main)で try-catch しています。
実行例2:独自例外を投げる
// Java 21 - 在庫チェックで独自例外を投げる例
public class StockService {
private int stock = 10;
public void reserve(int quantity) throws InsufficientStockException {
if (quantity > stock) {
throw new InsufficientStockException("在庫不足です。在庫: " + stock + ", 要求: " + quantity);
}
stock -= quantity;
System.out.println(quantity + "個予約しました。残り在庫: " + stock);
}
public static void main(String[] args) {
StockService service = new StockService();
try {
service.reserve(15); // 在庫10なので例外発生
} catch (InsufficientStockException e) {
System.err.println("エラー: " + e.getMessage());
}
}
}
実行結果(例):
エラー: 在庫不足です。在庫: 10, 要求: 15
InsufficientStockException をキャッチすることで、単なる Exception よりも具体的なエラー処理(在庫補充の通知など)が可能になります。
複数の例外を throws する
メソッドが複数種類の checked 例外を投げる可能性がある場合、カンマ区切りで列挙します。
// Java 21 - 複数の例外を宣言
import java.io.IOException;
import java.sql.SQLException;
public void processData(String filePath, String dbUrl) throws IOException, SQLException {
// ファイル読み込み → IOException
// DB接続 → SQLException
}
呼び出し側は両方をキャッチするか、さらに上位に throws します。
注意: throws で宣言する例外が増えすぎると、呼び出し元が複雑になります。関連する例外を親クラスでまとめるか、独自の unchecked 例外でラップする設計も検討しましょう。
プログラミングのベストプラクティス
| やるべきこと | 理由 |
|---|---|
| checked 例外は本当に回復可能なときだけ | 呼び出し側に無意味な try-catch を強制しない |
| 例外メッセージは具体的に | 「エラーが発生しました」だけでは原因不明 |
| ログに残してから再スロー | 上位で握り潰されても痕跡が残る |
| 例外を catch して無視しない | 空の catch ブロックは障害の温床 |
次のステップ
Java入門 #21 - 継承(extends)の基本 では、クラスの継承と super() でスーパークラスのコンストラクタを呼び出す方法を学びます。例外クラスの継承も同じ仕組みです。
参考リソース
- Oracle Java SE 21 - Throwable — 例外の親クラス
- Unchecked Exceptions — The Controversy — 公式チュートリアル
- Effective Java (Joshua Bloch) — 例外設計のベストプラクティス(第3版 Item 70-77)