6日目のゴール
6日目のテーマは
「例外処理を“アプリ全体の安全ネット”として設計する」 ことです。
ここまでであなたは、
例外の基本(try / catch)
事前チェックとの役割分担
throws で上の層に任せる設計
独自例外で「アプリの言葉」を持たせる
を一通り体に入れてきました。
6日目では、もう一歩だけ視点を引いて、
どこまで細かく try / catch を書くべきか
どこで“最後の砦”として例外を受け止めるか
ログ(記録)とユーザー向けメッセージをどう分けるか
を、小さな「メニュー付き例外処理アプリ」を通して整理します。
今日の題材:メニュー付き「ファイル or 割り算」アプリ
アプリのイメージ
今日は、これまでの二つをまとめたミニアプリを作ります。
「1: 割り算」「2: ファイル読み込み」「0: 終了」というメニューを表示する
1 を選んだら、割り算アプリ(例外あり)
2 を選んだら、ファイル読み込みアプリ(例外あり)
どちらも失敗する可能性がある
それでもアプリ全体は落とさず、メニューに戻る
ここで意識したいのは、
個々の機能の中で try / catch を書く
それでも漏れた例外を「一番外側」で受け止める
という二重の安全ネットです。
まずは「機能ごとの例外処理」を整理する
割り算機能をメソッドに切り出す
これまでの割り算アプリを、
メニューから呼べるメソッドにします。
import java.util.Scanner;
public class DivideFeature {
public static void run(Scanner scanner) {
System.out.println("=== 割り算機能 ===");
System.out.print("1つ目の整数を入力してください(キャンセルは空 Enter): ");
String aStr = scanner.nextLine();
if (aStr.isEmpty()) {
System.out.println("キャンセルしました。");
return;
}
System.out.print("2つ目の整数を入力してください: ");
String bStr = scanner.nextLine();
if (bStr.isEmpty()) {
System.out.println("キャンセルしました。");
return;
}
try {
int result = divideStrings(aStr, bStr);
System.out.println("結果: " + result);
} catch (NumberFormatException e) {
System.out.println("整数として解釈できない入力があります。数字だけを入力してください。");
} catch (ArithmeticException e) {
System.out.println("0 で割ることはできません。2つ目の数には 0 以外を入力してください。");
}
System.out.println("=== 割り算機能終了 ===");
}
static int divideStrings(String aStr, String bStr) {
int a = Integer.parseInt(aStr);
int b = Integer.parseInt(bStr);
return a / b;
}
}
Javaここでのポイントは、割り算機能の中で
「想定できる異常系」をすべて try / catch していることです。
数字でない → NumberFormatException
0 で割る → ArithmeticException
このレベルの例外は、
この機能の中で完結して扱うのが自然です。
ファイル読み込み機能もメソッドにする
5日目の FileLoader と FileLoadException を使って、
「ファイル読み込み機能」をメソッドにします。
import java.util.Scanner;
public class FileFeature {
public static void run(Scanner scanner) {
System.out.println("=== ファイル読み込み機能 ===");
System.out.print("読み込みたいファイル名を入力してください(キャンセルは空 Enter): ");
String fileName = scanner.nextLine();
if (fileName.isEmpty()) {
System.out.println("キャンセルしました。");
return;
}
try {
FileLoader.printFile(fileName);
System.out.println("=== 読み込み完了 ===");
} catch (FileLoadException e) {
System.out.println("ファイルを読み込めませんでした。ファイル名や権限を確認してください。");
System.out.println("(開発者向け情報)" + e.getMessage());
}
System.out.println("=== ファイル読み込み機能終了 ===");
}
}
Javaここでも、
「この機能にとって意味のある例外(FileLoadException)」を
この機能の中で完結して扱っています。
アプリ全体の“司会進行”と「最後の砦」
メニューを持つ Main を書く
次に、メニューを持つ Main を書きます。
import java.util.Scanner;
public class ExceptionApp {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
try {
runApp(scanner);
} catch (Exception e) {
System.out.println("予期しないエラーが発生しました。アプリを終了します。");
System.out.println("(開発者向け情報)" + e.getClass().getName() + ": " + e.getMessage());
} finally {
scanner.close();
}
}
private static void runApp(Scanner scanner) {
while (true) {
System.out.println();
System.out.println("=== 例外処理アプリ メニュー ===");
System.out.println("1: 割り算機能");
System.out.println("2: ファイル読み込み機能");
System.out.println("0: 終了");
System.out.print("番号を選んでください: ");
String choice = scanner.nextLine();
if ("0".equals(choice)) {
System.out.println("終了します。");
break;
} else if ("1".equals(choice)) {
DivideFeature.run(scanner);
} else if ("2".equals(choice)) {
FileFeature.run(scanner);
} else {
System.out.println("不正な入力です。0, 1, 2 のいずれかを入力してください。");
}
}
}
}
Javaここでの重要ポイントは二つです。
runApp(scanner) 全体を try { ... } catch (Exception e) で囲んでいる
これは「アプリ全体の最後の砦」としての catch である
つまり、
各機能の中で想定済みの例外は、その機能内で処理する
それでも漏れてきた“予期しない例外”は、ここでまとめて受け止める
という二段構えになっています。
「機能内で処理する例外」と「最後の砦」の違い
機能内の try / catch は“想定済みの失敗”用
割り算機能やファイル機能の中の try / catch は、
ユーザーがよくやりそうなミス
現実的に起こりうる失敗
それに対して、どう案内するか決めている
という、
「想定済みの異常系」 を扱っています。
ここでは、
ユーザーにとって分かりやすいメッセージを出し、
アプリ自体は続ける、という設計です。
最後の砦の catch は“想定外の失敗”用
一方、main の外側にある
try {
runApp(scanner);
} catch (Exception e) {
// 予期しないエラー
}
Javaは、
「本来ここまで来てほしくなかった例外」 を扱う場所です。
ここに来るのは、例えばこんなケースです。
自分のバグで NullPointerException が出た
思ってもみなかったライブラリの例外が飛んできた
機能内で catch し忘れた例外が漏れてきた
こういうときに、
アプリ全体が真っ赤なスタックトレースを出して落ちるのではなく
「予期しないエラーが発生しました」と一言伝えて終了する
開発者向けには、例外のクラス名とメッセージを表示する
という“最低限の品位”を保つための場所です。
ログとメッセージの分離を意識する
今はコンソール出力、現場ではログファイル
今回のコードでは、
開発者向け情報も System.out.println で出しています。
現場のアプリでは、ここが「ログ出力」に変わります。
ユーザー向け
→ 画面やコンソールに、短く分かりやすいメッセージ
開発者向け
→ ログファイルに、例外クラス名・メッセージ・スタックトレース
6日目で持っておいてほしい感覚は、
ユーザーに全部を見せる必要はない
でも、開発者が原因を追えるだけの情報は残すべき
というバランスです。
例外オブジェクトから取れる情報
Exception e からは、いろいろな情報が取れます。
e.getClass().getName() で例外のクラス名e.getMessage() でメッセージe.printStackTrace() でスタックトレース(どこで起きたかの履歴)
本番アプリでは、
これらをログに残しておくことで、
「ユーザーから“エラーが出た”と言われたときに、何が起きたか追える」
という状態を作ります。
どこまで try / catch を書くか、という設計の線引き
悪いパターン:全部の行を try / catch で囲む
初心者がやりがちなのが、
とにかくエラーが出た行を try / catch で囲む
何が起きても「エラーです」とだけ表示する
というパターンです。
これは、
コードが try / catch だらけで読みにくくなる
どこで何が失敗しているのか分からなくなる
異常系のストーリーが見えなくなる
という意味で、あまり良くありません。
今日の構造を“理想形のひとつ”として覚えておく
6日目でやった構造は、かなり現場寄りの形です。
機能ごとに「想定済みの例外」を try / catch する
想定外の例外は、アプリ全体の一番外側で catch する
ユーザー向けメッセージと開発者向け情報を分ける
この三つを意識しておけば、
例外処理が「場当たり的な消火活動」ではなく、
「アプリ全体の安全ネット」 として機能し始めます。
6日目で絶対に押さえてほしい本質
今日いちばん大事なのは、
「例外処理には“レベル”がある。機能レベルとアプリ全体レベルを意識して設計する」
という感覚です。
機能の中では、ユーザーがよくやるミスや現実的な失敗を想定して、丁寧に案内する
それでも漏れてくる予期しない例外は、一番外側でまとめて受け止め、アプリをきれいに終わらせる
ユーザーには短く分かりやすく、開発者には詳しく、という二重構造を意識する
ここまで来たあなたは、
もう「try / catch を知っている人」ではなく、
「例外処理をアプリの設計の一部として考えられる人」 です。
7日目では、この感覚を言葉にして整理し直して、
「自分はこういう方針で例外処理を書く」という軸を固めていきましょう。
