Java | 2週間で身につく、アプリを作りながら学ぶJavaの基本 - 11日目

Java Java
スポンサーリンク

11日目のゴールとテーマ

11日目のテーマは
「例外処理で“落ちないアプリ”に近づく」 です。

ここまでで、あなたは
クラス・オブジェクト・カプセル化・クラス同士の関係を使って、
小さなタスク管理アプリを形にしてきました。

今日はそこに、

ユーザーが変な入力をしても落ちない
想定外の状況でも、できるだけ丁寧にメッセージを出す
「エラーと仲良く付き合う」ための基本

を足していきます。

キーワードは
例外(Exception)try / catch です。


例外ってそもそも何か

「普通じゃないことが起きたときの“合図”」

Javaでプログラムを書いていると、
コンパイルは通るのに、実行中に突然こういうメッセージが出て落ちることがあります。

Exception in thread "main" ...

これが「例外(Exception)」です。

イメージとしては、

普通に処理を続けられないような“異常事態”が起きたときに
「もう無理!例外投げるから、誰かなんとかして!」

と、Javaが投げてくる“合図”です。

例えば、

0で割り算した
配列の範囲外にアクセスした
数字じゃない文字列を Integer.parseInt しようとした

こういうときに、例外が発生します。

例外を「何もせず放置」すると、
その場でプログラムは落ちます。

でも、try / catch を使うと、
「落とさずに、別の処理に切り替える」ことができます。


まずは典型的な例外を見てみる

数字に変換できない入力

タスク管理アプリで、
メニュー番号を int で受け取っていました。

int choice = scanner.nextInt();
Java

ここでユーザーが「abc」と入力したらどうなるか。
実行してみると、こんな感じになります。

java.util.InputMismatchException

これは、

「数字を期待していたのに、数字じゃないものが来た」

という例外です。

今までは「そういう入力をしない前提」で進めていましたが、
現実のアプリでは、ユーザーは平気で変な入力をします。

そこで、例外処理の出番です。


try / catch の基本形

「ここでエラーが出るかも」を囲う

try / catch の基本形はこうです。

try {
    // 例外が起きるかもしれない処理
} catch (ExceptionType e) {
    // 例外が起きたときにやる処理
}
Java

try の中で例外が発生すると、
その瞬間に catch にジャンプして、
中の処理が実行されます。

逆に、例外が発生しなければ、
catch はスキップされます。

さっきの nextInt() を例外対応してみましょう。

try {
    int choice = scanner.nextInt();
    scanner.nextLine();
    // choice を使った処理
} catch (java.util.InputMismatchException e) {
    System.out.println("数字を入力してください。");
    scanner.nextLine();  // 間違った入力を読み捨てる
}
Java

ここでのポイントを深掘りします。

try の中に「危ないかもしれない処理」を入れる
→ 今回は nextInt() がそれにあたる

catch (InputMismatchException e)
→ 「もし InputMismatchException が発生したら、ここに来てね」という意味
e は例外オブジェクト(詳細情報)だが、初心者のうちは使わなくてもOK

scanner.nextLine(); を catch の中でも呼んでいる
→ 間違った入力(”abc” など)がバッファに残っているので、それを捨てている

これで、「数字以外を入れられても落ちない」状態に一歩近づきます。


メニュー入力を安全にする

「数字が入るまで聞き続ける」メソッドを作る

毎回 try / catch を書くのは面倒なので、
「安全に int を読むメソッド」を1つ作ってしまいましょう。

import java.util.InputMismatchException;
import java.util.Scanner;

public class InputHelper {

    public static int readInt(Scanner scanner, String prompt) {
        while (true) {
            System.out.print(prompt);
            try {
                int value = scanner.nextInt();
                scanner.nextLine();  // 改行を読み飛ばす
                return value;
            } catch (InputMismatchException e) {
                System.out.println("数字を入力してください。");
                scanner.nextLine();  // 間違った入力を読み捨てる
            }
        }
    }
}
Java

ここでの重要ポイントを深掘りします。

while (true)
→ 「正しい数字が入るまで、永遠に聞き続ける」ループ
→ 正しい入力が来たら return で抜ける

prompt を引数で受け取っている
→ 「何を入力してほしいか」を呼び出し側が指定できる
→ 再利用性が高いメソッドになる

catch の中でメッセージを出して、ループを続ける
→ 間違った入力をしても、アプリは落ちずに「もう一度どうぞ」と言える

Main 側では、こう使えます。

int choice = InputHelper.readInt(scanner, "番号を選んでください: ");
Java

これで、メニュー入力がかなり安全になります。


タスク番号の入力も安全にする

「存在しない番号」をちゃんと扱う

タスクを完了にするときや削除するとき、
ユーザーに「タスクの番号」を入力してもらっていました。

ここでも、

数字以外が入る
存在しない番号が入る(0や100など)

というケースをちゃんと扱いたいです。

数字部分は InputHelper.readInt で解決できます。
あとは TaskManager 側で「範囲外」をチェックします。

TaskManager の completeTask はすでにこうなっていました。

public void completeTask(int number) {
    int index = number - 1;

    if (index < 0 || index >= tasks.size()) {
        System.out.println("その番号のタスクは存在しません。");
        return;
    }

    Task task = tasks.get(index);
    if (task.isDone()) {
        System.out.println("そのタスクはすでに完了しています。");
    } else {
        task.complete();
        System.out.println("タスク「" + task.getTitle() + "」を完了にしました。");
    }
}
Java

ここで、
「数字としては正しいが、タスクとしては存在しない」
というケースをちゃんとメッセージで返しています。

Main 側は、こう書き換えられます。

int number = InputHelper.readInt(scanner, "完了にするタスクの番号を入力してください: ");
manager.completeTask(number);
Java

数字としておかしい入力
→ InputHelper が面倒を見る

数字としてはOKだが、タスクとして存在しない番号
→ TaskManager が面倒を見る

という分担になっています。


例外処理の「やってはいけない」パターン

何でもかんでも catch(Exception) で握りつぶす

例外処理で初心者がやりがちな「危険な書き方」があります。

try {
    // いろいろな処理
} catch (Exception e) {
    // 何もしない
}
Java

あるいは、

catch (Exception e) {
    e.printStackTrace();
}
Java

のように、ただスタックトレースを出すだけ、というパターンです。

Exception は「ほぼすべての例外の親クラス」なので、
これを catch すると「何が起きてもここに来る」状態になります。

一見「全部捕まえてて偉い」ように見えますが、
実際には、

本当にヤバいエラーも握りつぶしてしまう
どこで何が起きたのか分かりにくくなる
ユーザーには意味のあるメッセージが出ない

という状態になりがちです。

今日の段階では、

「起こりうる例外の種類を意識して、
それに応じたメッセージや処理を書く」

という感覚を持っておけば十分です。


例外処理は「防御」ではなく「対話」

「エラーを潰す」のではなく「どう振る舞うかを決める」

例外処理というと、
「エラーを潰す」「落ちないようにする」
というイメージを持ちがちです。

でも、本質は少し違います。

「普通じゃないことが起きたときに、
アプリとしてどう振る舞うかを決める」

これが例外処理です。

例えば、

ユーザーが数字以外を入力した
→ 「数字を入力してください」と伝えて、もう一度聞く

存在しないタスク番号が指定された
→ 「その番号のタスクは存在しません」と伝える

ファイルが見つからなかった(12日目以降で出てくる話)
→ 「初回起動なので、まだデータがありません」と扱う

こういう「対話のデザイン」が、例外処理の本質です。


11日目のミニ仕上げ:例外対応版メニュー

メニュー入力を全部「安全版」に差し替える

ここまでの話を踏まえて、
Main のメニュー部分を「例外対応版」にしたイメージをまとめます。

import java.util.Scanner;

public class Main {

    public static void printMenu() {
        System.out.println("=== タスク管理アプリ(11日目版) ===");
        System.out.println("1: タスクを追加する");
        System.out.println("2: すべてのタスクを表示する");
        System.out.println("3: 未完了のタスクだけ表示する");
        System.out.println("4: 完了したタスクだけ表示する");
        System.out.println("5: タスクを完了にする");
        System.out.println("6: タスクを削除する");
        System.out.println("0: 終了する");
    }

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        TaskManager manager = new TaskManager();

        while (true) {
            printMenu();
            int choice = InputHelper.readInt(scanner, "番号を選んでください: ");

            if (choice == 0) {
                System.out.println("アプリを終了します。");
                break;
            } else if (choice == 1) {
                System.out.print("タスクのタイトルを入力してください: ");
                String title = scanner.nextLine();

                int priority = InputHelper.readInt(scanner, "優先度を入力してください(1:低, 2:中, 3:高): ");
                manager.addTask(title, priority);
            } else if (choice == 2) {
                manager.printAllTasks();
            } else if (choice == 3) {
                manager.printIncompleteTasks();
            } else if (choice == 4) {
                manager.printCompletedTasks();
            } else if (choice == 5) {
                int number = InputHelper.readInt(scanner, "完了にするタスクの番号を入力してください: ");
                manager.completeTask(number);
            } else if (choice == 6) {
                int number = InputHelper.readInt(scanner, "削除するタスクの番号を入力してください: ");
                manager.removeTask(number);
            } else {
                System.out.println("その番号は無効です。");
            }

            System.out.println();
        }
    }
}
Java

ここまで来ると、

数字以外を入れても落ちない
存在しない番号を入れても落ちない
ユーザーに対して、何がダメだったかをちゃんと伝えられる

という、「ちょっと大人なアプリ」になってきます。


11日目で一番大事な感覚

「エラーは“敵”じゃなくて、“会話のきっかけ”」

今日あなたに持ってほしい感覚はこれです。

エラーや例外は、
「失敗」ではなく「現実」です。

ユーザーは必ず変な入力をするし、
ファイルは見つからないこともあるし、
ネットワークは切れることもあります。

それを「想定外」として落ちるのではなく、
「そういうこともあるよね」と受け止めて、
アプリとしてどう振る舞うかを決める。

例外処理は、
そのための「会話の仕組み」です。


11日目のまとめと、12日目への予告

今日やったことを短く整理すると、

例外(Exception)は「普通じゃないことが起きたときの合図」
try / catch で「落とさずに別の処理に切り替える」ことができる
InputMismatchException を使って「数字以外の入力」に対応した
InputHelper.readInt で「安全な数値入力」を共通化した
TaskManager と組み合わせて、「存在しない番号」も丁寧に扱えるようにした

12日目は、
このタスク管理アプリに「保存」と「読み込み」の概念を足していきます。

アプリを終了してもタスクが消えない、
いわゆる「永続化」に、ファイル入出力を使って一歩踏み込みます。

タイトルとURLをコピーしました