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) {
// 例外が起きたときにやる処理
}
Javatry の中で例外が発生すると、
その瞬間に 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日目は、
このタスク管理アプリに「保存」と「読み込み」の概念を足していきます。
アプリを終了してもタスクが消えない、
いわゆる「永続化」に、ファイル入出力を使って一歩踏み込みます。
