初心者エンジニア必見!Javaの例外処理は3つの役割分担でスッキリ理解できる

こんにちは。ゆうせいです。

プログラミングを学んでいると、避けては通れないのがエラーへの対応ですよね。Javaではこれを例外処理と呼びますが、一つのクラスの中で完結させようとしてコードがごちゃごちゃになってしまった経験はありませんか。

実は、実務に近いきれいなコードを書くためには、例外をバケツリレーのように繋いでいく多段階の例外伝播という考え方がとても大切なんです。

今回は、数値を当てる数あてゲームを題材に、3つのクラスに役割を分けて例外処理をマスターする方法を解説します。皆さんは、エラーが起きたときに誰が責任を取るべきか、考えたことはありますか。

登場人物は3人!それぞれの役割を知ろう

例外処理をスムーズに行うためには、役割分担が鍵となります。まずは、今回登場する3つのクラスの役割を見ていきましょう。

クラス名役割例外処理のスタンス
Validator発生源(入力チェック)不正があれば throw して例外を発生させる
Kazuate中継役(ビジネスロジック)自分では解決せず、throws で上の階層に丸投げする
KazuateTest解決役(メイン画面)try-catch を使って、最後に責任を持って処理する

このように役割を分けることで、コードの見通しがぐっと良くなります。それでは、具体的な中身を見ていきましょう。

1. 例外の発生源:Validatorクラス

まずは、入力された値が正しいかどうかを判定する専用の道具、Validatorクラスです。このクラスは、ルールに反したものを見つけたら「これはおかしいですよ!」と声を上げる係です。

public class Validator {
    // 0〜10の範囲外なら、例外をインスタンス化して投げつける
    public void checkRange(int target) throws Exception {
        if (target < 0 || target > 10) {
            throw new Exception("【エラー】0から10の間で入力してください!");
        }
    }
}

ここでは throw という言葉が使われていますね。これは、自ら例外というボールを投げる動作を指します。高校の部活動で、ルール違反を見つけた審判が笛を吹くようなイメージだと考えてください。

2. 例外の中継役:Kazuateクラス

次に、ゲームのメインロジックを担当するKazuateクラスです。このクラスは、先ほどのValidatorを呼び出しますが、自分ではエラーを解決しません。

public class Kazuate {
    private int collectAnswer = 5;
    private String message;
    private Validator validator = new Validator();

    // throws Exception を書くことで「自分は責任を取りません」と宣言する
    public void checkAnswer(int answer) throws Exception {
        // ここで例外が発生すると、これ以降の処理はスキップされ、呼び出し元へ飛ぶ
        validator.checkRange(answer); 
        
        if (answer == collectAnswer) {
            this.message = "当たり!";
        } else {
            this.message = "ハズレ!";
        }
    }

    public String getMessage() { return message; }
    public int getAnswer() { return collectAnswer; }
}

注目すべきは、メソッド名の後ろにある throws Exception です。これは「もしエラーが起きても、私は責任を取りません。呼び出し元の人、よろしくお願いします!」という丸投げの宣言です。

なぜ自分で解決しないのでしょうか。それは、このクラスの仕事が「当たりか外れかを判定すること」であり、「エラーをどう画面に見せるか」は仕事ではないからです。

3. 例外の解決役:KazuateTestクラス

最後に、ユーザーと直接やり取りをするKazuateTestクラスです。ここが、すべての責任を引き受ける終着駅になります。

import java.util.Scanner;

public class KazuateTest {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        
        while(true) {
            try {
                Kazuate kazuate = new Kazuate();
                System.out.print("0-10の数字を入力: ");
                int answer = scanner.nextInt();
                
                // ここで例外が飛んでくる可能性がある
                kazuate.checkAnswer(answer);
                
                System.out.println(kazuate.getMessage());
                if(kazuate.getAnswer() == answer) {
                    System.out.println("終了します。");
                    break;
                }
            } catch (Exception e) {
                // ValidatorやKazuateから伝播してきたメッセージを表示
                System.out.println(e.getMessage());
                scanner.nextLine(); // 不正入力をクリア
            }
        }
    }
}

ここで登場するのが try-catch です。tryブロックの中で起きたエラーを、catchブロックがグローブのように受け止めます。どんなに遠く(Validator)から投げられた例外のボールも、最終的にはここでキャッチされ、ユーザーに優しいメッセージとして表示されるのです。

オリジナルの例外クラスを作る方法

Javaでカスタム例外を作るのは、実はとても簡単です。基本的には、既存のExceptionクラスを継承した新しいクラスを作るだけ!

数あてゲームをさらに進化させて、範囲外の数字が入ったとき専用の「InvalidNumberException」を作ってみましょう。

1. 例外クラスの定義(InvalidNumberException.java)

まずは、例外そのものを定義します。

// Exceptionを継承することで、Javaに「これは例外ですよ」と教える
public class InvalidNumberException extends Exception {
    // エラーメッセージを受け取るコンストラクタ
    public InvalidNumberException(String message) {
        super(message);
    }
}


ここでは extends を使って、例外の親玉である Exception の機能をすべて引き継いでいます。高校の数学で例えるなら、大きな「図形」というグループの中に、新しく「正三角形」という専門のルールを追加するようなものです。

2. 発生源の修正(Validator.java)

次に、この新しい例外を使うように Validator を書き換えます。

public class Validator {
    public void checkRange(int target) throws InvalidNumberException {
        if (target < 0 || target > 10) {
            // さっそく自作の例外を投げます!
            throw new InvalidNumberException("【範囲外エラー】" + target + "は0から10の間ではありません!");
        }
    }
}


throws の後ろが Exception から InvalidNumberException に変わりましたね。これにより、呼び出し側は「あ、数字の妥当性に関するエラーが飛んでくるんだな」と、コードを読むだけで理解できるようになります。

3. 中継役と解決役の対応

中継役(Kazuate)と解決役(KazuateTest)も、受け取る型を InvalidNumberException に合わせることで、より精密なエラーハンドリングが可能になります。

// KazuateTest内のcatch部分のイメージ
} catch (InvalidNumberException e) {
    System.out.println("入力値に問題があります: " + e.getMessage());
} catch (Exception e) {
    System.out.println("予期せぬエラーが発生しました。");
}

このように catch を分けることで、「入力ミスなら優しく注意」「それ以外の深刻なエラーなら管理者へ連絡」といった使い分けができるようになるんです。


役割を分けるメリットとデメリット

この構成には、実務で役立つ大きな利点があります。

メリット

  • 責任の所在がハッキリする:Validatorはチェックに専念し、Mainは表示に専念できます。もし将来、画面表示ではなく「ログファイルに書き出す」という仕様変更があっても、修正するのは解決役のクラスだけで済みます。
  • コードが読みやすくなる:中継役のKazuateクラスを見てください。もしここでエラーチェックの if 文をたくさん書いていたら、本来の「当たり判定」のロジックが埋もれてしまいますよね。例外伝播を使えば、メインの処理をスッキリ記述できます。

デメリット

  • 構造が少し複雑になる:初心者の方にとっては、エラーがどこからどこへ飛んでいくのか、追いかけるのが少し大変かもしれません。慣れるまでは、頭の中でバケツリレーの図を描いてみるのがコツです。


まとめと今後のステップ

例外処理の多段階伝播について、イメージは掴めましたか。

エラーが発生したとき、Validator から Kazuate、そして KazuateTest へと遡っていく様子をコールスタックと呼びます。デバッグの際は、この積み重なった履歴を読み解くことが、プロのエンジニアへの第一歩です。

まずは、実際にこのコードを動かしてみてください。そして、わざと範囲外の数字を入力して、どのメソッドからメッセージが届いているのかを追跡してみましょう!

セイ・コンサルティング・グループでは新人エンジニア研修のアシスタント講師を募集しています。

投稿者プロフィール

山崎講師
山崎講師代表取締役
セイ・コンサルティング・グループ株式会社代表取締役。
岐阜県出身。
2000年創業、2004年会社設立。
IT企業向け人材育成研修歴業界歴20年以上。
すべての無駄を省いた費用対効果の高い「筋肉質」な研修を提供します!
この記事に間違い等ありましたらぜひお知らせください。

学生時代は趣味と実益を兼ねてリゾートバイトにいそしむ。長野県白馬村に始まり、志賀高原でのスキーインストラクター、沖縄石垣島、北海道トマム。高じてオーストラリアのゴールドコーストでツアーガイドなど。現在は野菜作りにはまっている。