この記事で学ぶこと
この記事では、例外の委譲(delegation)と伝播(propagation) について、高校生にもわかりやすく解説します。
今回のゴール
- 委譲と伝播の違いを理解する
- throws宣言の本当の意味を知る
- 例外がどのように上位メソッドに渡されるか理解する
- Java資格試験レベルの問題が解けるようになる
- 実践的な例外処理設計ができるようになる
難易度: ★★★☆☆
学習時間: 約30分
委譲と伝播って何?
まず身近な例で理解しよう
学校での例
【状況】クラスで問題が発生した時
1️⃣ 委譲(delegation)の例
生徒 → 学級委員:「この問題、先生に報告してください」
学級委員 → 先生:「クラスで問題が起きました」
→ 学級委員が責任を先生に「委ねる」
2️⃣ 伝播(propagation)の例
教室 → 学年主任 → 副校長 → 校長
→ 問題が自動的に上の階層に「伝わっていく」
Javaでの委譲と伝播
委譲(Delegation)
// メソッドが例外を「投げる」と宣言する
public void readFile() throws IOException {
// 「私はIOExceptionを処理しません。呼び出し元にお任せします」
}
伝播(Propagation)
// 例外が呼び出し元に自動的に「伝わる」
methodA() → methodB() → methodC()
← Exception ←
委譲(Delegation)の詳細解説
委譲とは何か
委譲 = 「この例外は私では処理できません。呼び出し元にお願いします」
基本的な委譲の例
import java.io.*;
public class FileDelegation {
// 例外を委譲する(throws宣言)
public void readConfigFile() throws IOException {
FileReader fr = new FileReader("config.txt");
BufferedReader br = new BufferedReader(fr);
String line = br.readLine(); // IOException が発生する可能性
br.close();
// ここで IOException をキャッチしない
// → 呼び出し元に委譲される
}
// 呼び出し元は委譲された例外を処理する必要がある
public void useConfig() {
try {
readConfigFile(); // IOException を受け取る可能性
} catch (IOException e) {
System.out.println("設定ファイルが読めません: " + e.getMessage());
}
}
}
throws宣言の本当の意味
// ❌ 間違った理解
// 「このメソッドは例外を投げる」
// ✅ 正しい理解
// 「このメソッドで発生した例外は、呼び出し元が処理してください」
public void methodName() throws ExceptionType {
// 例外が発生したら、呼び出し元に委譲する
}
複数の例外を委譲する
public void complexOperation() throws IOException, SQLException, InterruptedException {
// 3つの異なる例外を呼び出し元に委譲
// ファイル操作(IOException の可能性)
FileReader fr = new FileReader("data.txt");
// データベース操作(SQLException の可能性)
Connection conn = DriverManager.getConnection("...");
// スレッド操作(InterruptedException の可能性)
Thread.sleep(1000);
}
伝播(Propagation)の詳細解説
伝播とは何か
伝播 = 例外が呼び出しチェーンを遡って自動的に上位メソッドに伝わること
伝播の視覚的な例
ExceptionPropagation.java
public class ExceptionPropagation {
public void levelA() {
try {
levelB();
} catch (RuntimeException e) {
System.out.println("レベルAで例外をキャッチ: " + e.getMessage());
}
}
public void levelB() {
levelC(); // 例外が発生したら、ここを通ってlevelAに伝播
}
public void levelC() {
levelD(); // 例外が発生したら、ここを通ってlevelBに伝播
}
public void levelD() {
// ここで例外が発生
throw new RuntimeException("レベルDで問題発生!");
// 伝播の流れ:
// levelD → levelC → levelB → levelA (catch)
}
public static void main(String[] args) {
ExceptionPropagation demo = new ExceptionPropagation();
demo.levelA();
// 出力: レベルAで例外をキャッチ: レベルDで問題発生!
}
}
スタックトレースで伝播を確認
PropagationTrace.java
public class PropagationTrace {
public static void main(String[] args) {
PropagationTrace demo = new PropagationTrace();
demo.method1();
}
public void method1() {
method2();
}
public void method2() {
method3();
}
public void method3() {
throw new RuntimeException("問題発生");
}
}
実行結果(スタックトレース):
Exception in thread "main" java.lang.RuntimeException: 問題発生
at PropagationTrace.method3(PropagationTrace.java:17) ← 発生地点
at PropagationTrace.method2(PropagationTrace.java:13) ← 伝播
at PropagationTrace.method1(PropagationTrace.java:9) ← 伝播
at PropagationTrace.main(PropagationTrace.java:5) ← 最終到達点
チェック例外の伝播と委譲
import java.io.*;
public class CheckedExceptionFlow {
public static void main(String[] args) {
CheckedExceptionFlow demo = new CheckedExceptionFlow();
demo.handleData();
}
// 最終的に例外を処理
public void handleData() {
try {
readData(); // すべての委譲された例外がここに到着
} catch (IOException e) {
System.out.println("ファイル操作エラー: " + e.getMessage());
}
}
// レベル1: 例外を委譲
public void readData() throws IOException {
processFile(); // IOException を受け取り、さらに委譲
}
// レベル2: 例外を委譲
public void processFile() throws IOException {
openFile(); // IOException を受け取り、さらに委譲
}
// レベル3: 例外が実際に発生
public void openFile() throws IOException {
throw new IOException("ファイルが見つかりません");
}
}
実行結果
ファイル操作エラー: ファイルが見つかりません
委譲と伝播の組み合わせ
実際のプログラムでの動作
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
public class DelegationAndPropagation {
// プレゼンテーション層
public void handleUserRequest(String input) {
try {
processUserData(input); // 複数の例外を受け取る
System.out.println("処理が完了しました");
} catch (ValidationException e) {
System.out.println("入力エラー: " + e.getMessage());
} catch (SQLException e) {
System.out.println("データベースエラー: " + e.getMessage());
}
}
// ビジネスロジック層
public void processUserData(String userData) throws SQLException, ValidationException {
// 入力検証
if (userData == null || userData.isEmpty()) {
throw new ValidationException("ユーザーデータが無効です");
}
// データベースに保存(SQLException を委譲)
saveToDatabase(userData);
}
// データベースアクセス層
public void saveToDatabase(String data) throws SQLException{
// SQL例外を委譲
Connection conn = DriverManager.getConnection("host:port/dbname", "user", "password");
PreparedStatement stmt = conn.prepareStatement("INSERT...");
stmt.executeUpdate(); // SQLException の可能性
}
}
// カスタム例外
class ValidationException extends Exception {
public ValidationException(String message) {
super(message);
}
}
RPGシステムでの実践例
ゲーム保存システムでの委譲と伝播
import java.io.*;
public class GameSaveSystem {
// レベル1: ゲームデータ保存(委譲)
public void saveGame(Player player) throws GameSaveException {
try {
savePlayerData(player); // IOException を受け取る可能性
saveInventoryData(player); // IOException を受け取る可能性
saveQuestData(player); // IOException を受け取る可能性
} catch (IOException e) {
// IOException を GameSaveException でラップして委譲
throw new GameSaveException("ゲーム保存に失敗しました", e);
}
}
// レベル2: プレイヤーデータ保存(委譲)
public void savePlayerData(Player player) throws IOException {
writeToFile("player.dat", player.toSaveData());
}
// レベル3: インベントリデータ保存(委譲)
public void saveInventoryData(Player player) throws IOException {
writeToFile("inventory.dat", player.getInventory().toSaveData());
}
// レベル4: クエストデータ保存(委譲)
public void saveQuestData(Player player) throws IOException {
writeToFile("quest.dat", player.getQuestProgress().toSaveData());
}
// 実際のファイル書き込み(実際の例外発生地点)
private void writeToFile(String filename, String data) throws IOException {
FileWriter fw = new FileWriter(filename);
fw.write(data);
fw.close(); // IOException が発生する可能性
}
// ゲームメインループ(最終処理)
public void gameLoop() {
Player player = getCurrentPlayer();
try {
saveGame(player); // GameSaveException を受け取る
System.out.println("ゲームを保存しました");
} catch (GameSaveException e) {
System.out.println("エラー: " + e.getMessage());
System.out.println("原因: " + e.getCause().getMessage());
// プレイヤーに再試行を促す
showSaveErrorDialog();
}
}
}
// ゲーム固有の例外
class GameSaveException extends Exception {
public GameSaveException(String message, Throwable cause) {
super(message, cause);
}
}
委譲と伝播の使い分け
いつ委譲を使うか
委譲を使う場面
1.下位層で適切な処理ができない
// データアクセス層では UI エラー表示ができない
public void loadUserData(int userId) throws DataNotFoundException {
// データが見つからない場合、UI層に委譲
if (userData == null) {
throw new DataNotFoundException("ユーザーID: " + userId);
}
}
2.複数の処理方法がある
// キャッシュに失敗した場合の処理を呼び出し元に委譲
public void cacheData(String key, Object data) throws CacheException {
// キャッシュ失敗は呼び出し元が判断
// - 再試行する?
// - 無視する?
// - 別の方法を試す?
}
3.レイヤー間の責任分離
// ビジネスロジック層
public void transferMoney(Account from, Account to, BigDecimal amount)
throws InsufficientFundsException, AccountNotFoundException {
// UI層が決定すること:
// - エラーメッセージの表示方法
// - 再試行の可否
// - ログ出力の詳細度
}
❌ 委譲を避ける場面
1.その場で適切に処理できる
// ✅ 適切な例
public void processConfig() {
try {
loadConfig();
} catch (IOException e) {
// デフォルト設定で継続(適切な回復処理)
useDefaultConfig();
logger.warn("設定ファイルを読み込めませんでした。デフォルト設定を使用します。");
}
}
2.過度な委譲
// ❌ 良くない例:何でも委譲
public void method1() throws Exception { // 汎用すぎる
method2();
}
public void method2() throws Exception { // 汎用すぎる
method3();
}
// ✅ 改善版:適切な例外型を使用
public void method1() throws SpecificBusinessException {
method2();
}
☕ Java資格試験によく出る問題
問題1:委譲の基本
public class QuizDelegation {
public void methodA() {
try {
methodB();
} catch (IOException e) {
System.out.println("A");
}
}
public void methodB() throws IOException {
methodC();
}
public void methodC() {
throw new RuntimeException("エラー");
}
public static void main(String[] args) {
new QuizDelegation().methodA();
}
}
問題: 上記のコードを実行すると何が出力されますか?
解答と解説を見る
解答: 例外が発生してプログラムが終了
解説:
-
methodC()でRuntimeExceptionが発生 -
methodB()はIOExceptionしか委譲宣言していない -
RuntimeExceptionは非チェック例外なので、委譲宣言なしで伝播 -
methodA()はIOExceptionしかキャッチしていない -
RuntimeExceptionがキャッチされず、プログラムが終了
重要ポイント:
- チェック例外は委譲宣言が必要
- 非チェック例外は委譲宣言なしで伝播
- キャッチする例外型と実際の例外型が一致する必要がある
問題2:例外の伝播順序
public class QuizPropagation {
public void method1() {
try {
method2();
} catch (Exception e) {
System.out.println("method1で例外をキャッチ");
} finally {
System.out.println("method1のfinally");
}
}
public void method2() {
try {
method3();
} finally {
System.out.println("method2のfinally");
}
}
public void method3() {
try {
throw new RuntimeException("エラー発生");
} finally {
System.out.println("method3のfinally");
}
}
public static void main(String[] args) {
new QuizPropagation().method1();
}
}
問題: 実行結果を順番に答えてください。
解答と解説を見る
解答:
method3のfinally
method2のfinally
method1で例外をキャッチ
method1のfinally
解説:
-
method3()で例外発生 →method3のfinally実行 - 例外が
method2()に伝播 →method2のfinally実行 - 例外が
method1()に伝播 → catch節で捕捉 →method1で例外をキャッチ出力 -
method1()の finally実行 →method1のfinally出力
重要ポイント:
- finally節は例外が発生しても必ず実行される
- 例外の伝播中も、経由するメソッドのfinally節は実行される
- finally節は、そのメソッドを抜ける直前に実行される
委譲と伝播の可視化
委譲の流れ図
伝播の流れ図
📚 まとめ
委譲と伝播の要点整理
| 項目 | 委譲(Delegation) | 伝播(Propagation) |
|---|---|---|
| 目的 | 責任の移譲 | 例外の自動転送 |
| 宣言 | throws 必要(チェック例外) | 自動的に発生 |
| 制御 | プログラマーが決定 | システムが自動実行 |
| 使用場面 | 適切な処理層まで委譲 | 例外の自然な流れ |
実践で使える設計パターン
1.レイヤード委譲パターン
// プレゼンテーション層 → ビジネス層 → データ層
UI Layer → Service Layer → Repository Layer
↑ ↑ ↑
try-catch throws throws
2.例外変換パターン
// 下位の技術的例外を上位のビジネス例外に変換
try {
lowLevelOperation();
} catch (SQLException e) {
throw new BusinessLogicException("業務処理エラー", e);
}
3.フォールバックパターン
// 複数の方法を試し、最後に例外報告
try {
primaryMethod();
} catch (Exception e1) {
try {
secondaryMethod();
} catch (Exception e2) {
throw new AllMethodsFailedException(e1, e2);
}
}
Java資格試験のポイント
✅ 覚えておくべきこと
- チェック例外は throws 宣言必須
- 非チェック例外は throws 宣言不要(でも書いてもOK)
- finally 節は例外があっても実行される
- 例外の型は正確に一致する必要がある
- オーバーライド時は親クラス以下の例外しか宣言できない
❌ よくある間違い
- throws Exception(汎用すぎる)
- 例外を隠蔽(catch したのに何もしない)
- 不適切な例外型(RuntimeException を多用)
関連記事
Java Quest シリーズ
参考資料
最後に
例外の委譲と伝播は、最初は難しく感じるかもしれませんが、「責任の分担」 という考え方で理解すると簡単です。
- 委譲: 「私には処理できないので、上の人にお願いします」
- 伝播: 「問題が自動的に上の階層に伝わります」
この概念をマスターすると、より良い例外処理設計ができるようになり、保守性の高いコードが書けるようになります。
本記事は教育目的で作成されています。RPGの例を通じてプログラミング概念を楽しく学習できることを目指しています。


