はじめに
Java研修コーディング問題集の第9回は 例外処理 です。
プログラムの実行中に発生するエラーを適切に処理することは、堅牢なアプリケーションを作るために欠かせません。try-catch-finally、独自例外、例外の伝播について学びましょう。
難易度の見方
| マーク | 難易度 | 目安 |
|---|---|---|
| ⭐ | 基本 | 研修1週目レベル |
| ⭐⭐ | 応用 | 少し考える必要あり |
| ⭐⭐⭐ | チャレンジ | 複数の知識を組み合わせる |
問題1:基本的な try-catch ⭐
問題
以下のコードは実行時エラーが発生します。try-catch で適切に例外を処理してください。
int[] numbers = {1, 2, 3};
System.out.println(numbers[5]); // ArrayIndexOutOfBoundsException
String str = null;
System.out.println(str.length()); // NullPointerException
int result = 10 / 0; // ArithmeticException
期待する出力
=== 例外処理の基本 ===
エラー: 配列のインデックスが範囲外です(Index 5 out of bounds for length 3)
エラー: null参照です
エラー: ゼロ除算です(/ by zero)
すべての処理が完了しました
模範解答
public class Exercise01 {
public static void main(String[] args) {
System.out.println("=== 例外処理の基本 ===");
// ArrayIndexOutOfBoundsException
try {
int[] numbers = {1, 2, 3};
System.out.println(numbers[5]);
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("エラー: 配列のインデックスが範囲外です(" + e.getMessage() + ")");
}
// NullPointerException
try {
String str = null;
System.out.println(str.length());
} catch (NullPointerException e) {
System.out.println("エラー: null参照です");
}
// ArithmeticException
try {
int result = 10 / 0;
} catch (ArithmeticException e) {
System.out.println("エラー: ゼロ除算です(" + e.getMessage() + ")");
}
System.out.println("すべての処理が完了しました");
}
}
ポイント: try ブロックで例外が発生する可能性のあるコードを囲み、catch で例外を捕捉します。e.getMessage() で例外のメッセージを取得できます。例外を処理した後も、プログラムは続行されます。
問題2:finally ブロック ⭐
問題
データベース接続のシミュレーションを行い、finally ブロックで必ず切断処理が行われることを確認してください。
期待する出力
=== 正常時 ===
データベースに接続しました
データを取得しました: [売上データ]
データベースを切断しました
=== エラー時 ===
データベースに接続しました
エラー: データの取得に失敗しました
データベースを切断しました
模範解答
public class Exercise02 {
public static void main(String[] args) {
System.out.println("=== 正常時 ===");
queryDatabase(true);
System.out.println("\n=== エラー時 ===");
queryDatabase(false);
}
static void queryDatabase(boolean success) {
System.out.println("データベースに接続しました");
try {
if (!success) {
throw new RuntimeException("データの取得に失敗しました");
}
System.out.println("データを取得しました: [売上データ]");
} catch (RuntimeException e) {
System.out.println("エラー: " + e.getMessage());
} finally {
System.out.println("データベースを切断しました");
}
}
}
ポイント: finally ブロックは例外の有無に関わらず必ず実行されます。データベース接続やファイルハンドルなどのリソースの解放処理に使います。
問題3:複数の例外をキャッチ ⭐
問題
文字列を数値に変換する処理で、さまざまな入力に対して適切な例外処理を行ってください。
入力値:"123", "abc", "99999999999", null, "45"
期待する出力
=== 数値変換 ===
"123" → 成功: 123
"abc" → エラー: 数値形式が不正です
"99999999999" → エラー: 数値形式が不正です
null → エラー: null が渡されました
"45" → 成功: 45
変換成功: 2件 / 失敗: 3件
模範解答
public class Exercise03 {
public static void main(String[] args) {
String[] inputs = {"123", "abc", "99999999999", null, "45"};
int successCount = 0;
int failCount = 0;
System.out.println("=== 数値変換 ===");
for (String input : inputs) {
try {
if (input == null) {
throw new NullPointerException("null が渡されました");
}
int value = Integer.parseInt(input);
System.out.println("\"" + input + "\" → 成功: " + value);
successCount++;
} catch (NumberFormatException e) {
System.out.println("\"" + input + "\" → エラー: 数値形式が不正です");
failCount++;
} catch (NullPointerException e) {
System.out.println("null → エラー: " + e.getMessage());
failCount++;
}
}
System.out.println("変換成功: " + successCount + "件 / 失敗: " + failCount + "件");
}
}
ポイント: 複数の catch ブロックで異なる例外を個別に処理できます。Java 7以降では catch (NumberFormatException | NullPointerException e) のようにマルチキャッチも可能です。
問題4:例外の伝播(throws)⭐⭐
問題
メソッドの呼び出しチェーンで例外が伝播する様子を確認してください。
methodA() → methodB() → methodC() の順に呼び出し、methodC() で発生した例外が methodA() まで伝播する流れを出力してください。
期待する出力
=== 例外の伝播 ===
methodA: 開始
methodB: 開始
methodC: 開始
methodC: 例外を投げます
methodB: methodCから例外を受け取りました
methodA: methodBから例外をキャッチしました
エラー内容: methodCで問題が発生しました
methodA: 終了
模範解答
public class Exercise04 {
public static void main(String[] args) {
System.out.println("=== 例外の伝播 ===");
methodA();
}
static void methodA() {
System.out.println("methodA: 開始");
try {
methodB();
} catch (Exception e) {
System.out.println("methodA: methodBから例外をキャッチしました");
System.out.println("エラー内容: " + e.getMessage());
}
System.out.println("methodA: 終了");
}
static void methodB() throws Exception {
System.out.println(" methodB: 開始");
try {
methodC();
} catch (Exception e) {
System.out.println(" methodB: methodCから例外を受け取りました");
throw e; // 再スロー
}
}
static void methodC() throws Exception {
System.out.println(" methodC: 開始");
System.out.println(" methodC: 例外を投げます");
throw new Exception("methodCで問題が発生しました");
}
}
ポイント: throws でメソッドが例外を投げる可能性を宣言します。キャッチした例外を throw e で再スローすると、上位の呼び出し元に例外を伝播できます。例外の処理は、適切なレベルで行いましょう。
問題5:独自例外クラス ⭐⭐
問題
年齢のバリデーションを行う独自例外クラス InvalidAgeException を作成してください。
- 0未満の場合:「年齢は0以上である必要があります」
- 150超の場合:「年齢は150以下である必要があります」
期待する出力
=== 年齢バリデーション ===
年齢 25: OK
年齢 -5: エラー - 年齢は0以上である必要があります(入力値: -5)
年齢 200: エラー - 年齢は150以下である必要があります(入力値: 200)
年齢 0: OK
年齢 150: OK
模範解答
class InvalidAgeException extends Exception {
private int age;
InvalidAgeException(String message, int age) {
super(message);
this.age = age;
}
int getAge() {
return age;
}
}
public class Exercise05 {
public static void main(String[] args) {
int[] ages = {25, -5, 200, 0, 150};
System.out.println("=== 年齢バリデーション ===");
for (int age : ages) {
try {
validateAge(age);
System.out.println("年齢 " + age + ": OK");
} catch (InvalidAgeException e) {
System.out.println("年齢 " + age + ": エラー - " + e.getMessage() + "(入力値: " + e.getAge() + ")");
}
}
}
static void validateAge(int age) throws InvalidAgeException {
if (age < 0) {
throw new InvalidAgeException("年齢は0以上である必要があります", age);
}
if (age > 150) {
throw new InvalidAgeException("年齢は150以下である必要があります", age);
}
}
}
ポイント: 独自例外クラスは Exception(検査例外)または RuntimeException(非検査例外)を継承して作ります。追加のフィールド(ここでは age)を持たせることで、エラーの詳細情報を呼び出し元に伝えられます。
問題6:try-with-resources ⭐⭐
問題
AutoCloseable インターフェースを実装した DatabaseConnection クラスを作成し、try-with-resources で自動的にリソースが解放されることを確認してください。
期待する出力
=== try-with-resources ===
--- 正常時 ---
DB接続を開きました(ID: DB-001)
クエリ実行: SELECT * FROM users
DB接続を閉じました(ID: DB-001)
--- 例外時 ---
DB接続を開きました(ID: DB-002)
エラー: テーブルが見つかりません
DB接続を閉じました(ID: DB-002)
模範解答
class DatabaseConnection implements AutoCloseable {
private String connectionId;
DatabaseConnection(String connectionId) {
this.connectionId = connectionId;
System.out.println("DB接続を開きました(ID: " + connectionId + ")");
}
void executeQuery(String sql) throws Exception {
if (sql.contains("invalid_table")) {
throw new Exception("テーブルが見つかりません");
}
System.out.println("クエリ実行: " + sql);
}
@Override
public void close() {
System.out.println("DB接続を閉じました(ID: " + connectionId + ")");
}
}
public class Exercise06 {
public static void main(String[] args) {
System.out.println("=== try-with-resources ===");
// 正常時
System.out.println("\n--- 正常時 ---");
try (DatabaseConnection db = new DatabaseConnection("DB-001")) {
db.executeQuery("SELECT * FROM users");
} catch (Exception e) {
System.out.println("エラー: " + e.getMessage());
}
// 例外時
System.out.println("\n--- 例外時 ---");
try (DatabaseConnection db = new DatabaseConnection("DB-002")) {
db.executeQuery("SELECT * FROM invalid_table");
} catch (Exception e) {
System.out.println("エラー: " + e.getMessage());
}
}
}
ポイント: try-with-resources(Java 7以降)は AutoCloseable を実装したリソースを自動的に閉じます。try (リソース) の括弧内で宣言すると、ブロック終了時に自動で close() が呼ばれます。例外が発生しても確実にリソースが解放されます。
問題7:例外チェーン ⭐⭐
問題
低レベルの例外を高レベルの例外でラップして、例外の原因チェーンを作成してください。
期待する出力
=== 例外チェーン ===
ビジネスエラー: ユーザー登録に失敗しました
原因: データベースエラー: INSERTクエリの実行に失敗
根本原因: java.net.ConnectException: データベースサーバーに接続できません
模範解答
import java.net.ConnectException;
class DatabaseException extends Exception {
DatabaseException(String message, Throwable cause) {
super(message, cause);
}
}
class BusinessException extends Exception {
BusinessException(String message, Throwable cause) {
super(message, cause);
}
}
public class Exercise07 {
public static void main(String[] args) {
System.out.println("=== 例外チェーン ===");
try {
registerUser("田中太郎");
} catch (BusinessException e) {
System.out.println("ビジネスエラー: " + e.getMessage());
Throwable cause = e.getCause();
if (cause != null) {
System.out.println(" 原因: " + cause.getMessage());
Throwable rootCause = cause.getCause();
if (rootCause != null) {
System.out.println(" 根本原因: " + rootCause.getClass().getName() + ": " + rootCause.getMessage());
}
}
}
}
static void registerUser(String name) throws BusinessException {
try {
insertToDatabase(name);
} catch (DatabaseException e) {
throw new BusinessException("ユーザー登録に失敗しました", e);
}
}
static void insertToDatabase(String name) throws DatabaseException {
try {
connectToServer();
} catch (ConnectException e) {
throw new DatabaseException("データベースエラー: INSERTクエリの実行に失敗", e);
}
}
static void connectToServer() throws ConnectException {
throw new ConnectException("データベースサーバーに接続できません");
}
}
ポイント: 例外チェーンは低レベルの例外を高レベルの例外でラップする手法です。new Exception(message, cause) のように原因となる例外を渡し、getCause() で取得できます。実務ではログ出力やデバッグ時に根本原因の追跡に役立ちます。
問題8:検査例外と非検査例外 ⭐⭐
問題
以下のコードの各例外が「検査例外」か「非検査例外」かを判定し、それぞれ適切に処理してください。
期待する出力
=== 例外の分類 ===
--- 非検査例外(RuntimeException系)---
NullPointerException: 非検査例外 → catchは任意
ArrayIndexOutOfBoundsException: 非検査例外 → catchは任意
ArithmeticException: 非検査例外 → catchは任意
IllegalArgumentException: 非検査例外 → catchは任意
--- 検査例外(Exception系)---
IOException: 検査例外 → catch必須
ClassNotFoundException: 検査例外 → catch必須
模範解答
import java.io.IOException;
public class Exercise08 {
public static void main(String[] args) {
System.out.println("=== 例外の分類 ===");
System.out.println("\n--- 非検査例外(RuntimeException系)---");
try {
throw new NullPointerException("テスト");
} catch (NullPointerException e) {
System.out.println("NullPointerException: 非検査例外 → catchは任意");
}
try {
throw new ArrayIndexOutOfBoundsException("テスト");
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("ArrayIndexOutOfBoundsException: 非検査例外 → catchは任意");
}
try {
throw new ArithmeticException("テスト");
} catch (ArithmeticException e) {
System.out.println("ArithmeticException: 非検査例外 → catchは任意");
}
try {
throw new IllegalArgumentException("テスト");
} catch (IllegalArgumentException e) {
System.out.println("IllegalArgumentException: 非検査例外 → catchは任意");
}
System.out.println("\n--- 検査例外(Exception系)---");
try {
throw new IOException("テスト");
} catch (IOException e) {
System.out.println("IOException: 検査例外 → catch必須");
}
try {
throw new ClassNotFoundException("テスト");
} catch (ClassNotFoundException e) {
System.out.println("ClassNotFoundException: 検査例外 → catch必須");
}
}
}
ポイント:
| 分類 | 親クラス | catch | 代表例 |
|---|---|---|---|
| 検査例外 | Exception |
必須 | IOException, SQLException |
| 非検査例外 | RuntimeException |
任意 | NullPointerException, IllegalArgumentException |
| エラー | Error |
通常catchしない | OutOfMemoryError, StackOverflowError |
検査例外は try-catch または throws での処理がコンパイラに強制されます。非検査例外はプログラムのバグに起因することが多いです。
問題9:バリデーションフレームワーク ⭐⭐⭐
問題
ユーザー登録のバリデーションを行うシステムを作成してください。複数のバリデーションエラーをまとめて報告できるようにしてください。
バリデーションルール:
- 名前:null でない、空でない、20文字以下
- メールアドレス:null でない、
@を含む - 年齢:0以上150以下
期待する出力
=== ユーザー登録バリデーション ===
--- ケース1: 正常 ---
入力: {name=田中太郎, email=tanaka@example.com, age=25}
バリデーション成功!
--- ケース2: 複数エラー ---
入力: {name=, email=invalid-email, age=-5}
バリデーションエラー(3件):
1. 名前は空にできません
2. メールアドレスに@が含まれていません
3. 年齢は0以上である必要があります
--- ケース3: null値 ---
入力: {name=null, email=null, age=200}
バリデーションエラー(3件):
1. 名前はnullにできません
2. メールアドレスはnullにできません
3. 年齢は150以下である必要があります
模範解答
class ValidationException extends Exception {
private String[] errors;
ValidationException(String[] errors) {
super("バリデーションエラー(" + errors.length + "件)");
this.errors = errors;
}
String[] getErrors() {
return errors;
}
}
class UserValidator {
static void validate(String name, String email, int age) throws ValidationException {
// エラーを一時的に格納(最大10件)
String[] tempErrors = new String[10];
int errorCount = 0;
// 名前のバリデーション
if (name == null) {
tempErrors[errorCount++] = "名前はnullにできません";
} else if (name.isEmpty()) {
tempErrors[errorCount++] = "名前は空にできません";
} else if (name.length() > 20) {
tempErrors[errorCount++] = "名前は20文字以下である必要があります";
}
// メールのバリデーション
if (email == null) {
tempErrors[errorCount++] = "メールアドレスはnullにできません";
} else if (!email.contains("@")) {
tempErrors[errorCount++] = "メールアドレスに@が含まれていません";
}
// 年齢のバリデーション
if (age < 0) {
tempErrors[errorCount++] = "年齢は0以上である必要があります";
} else if (age > 150) {
tempErrors[errorCount++] = "年齢は150以下である必要があります";
}
if (errorCount > 0) {
String[] errors = new String[errorCount];
for (int i = 0; i < errorCount; i++) {
errors[i] = tempErrors[i];
}
throw new ValidationException(errors);
}
}
}
public class Exercise09 {
public static void main(String[] args) {
System.out.println("=== ユーザー登録バリデーション ===");
// ケース1: 正常
System.out.println("\n--- ケース1: 正常 ---");
tryRegister("田中太郎", "tanaka@example.com", 25);
// ケース2: 複数エラー
System.out.println("\n--- ケース2: 複数エラー ---");
tryRegister("", "invalid-email", -5);
// ケース3: null値
System.out.println("\n--- ケース3: null値 ---");
tryRegister(null, null, 200);
}
static void tryRegister(String name, String email, int age) {
System.out.println("入力: {name=" + name + ", email=" + email + ", age=" + age + "}");
try {
UserValidator.validate(name, email, age);
System.out.println("バリデーション成功!");
} catch (ValidationException e) {
System.out.println(e.getMessage() + ":");
String[] errors = e.getErrors();
for (int i = 0; i < errors.length; i++) {
System.out.println(" " + (i + 1) + ". " + errors[i]);
}
}
}
}
ポイント: バリデーションではエラーを1つ見つけた時点で処理を中断するのではなく、すべてのエラーをまとめて報告するのがユーザーフレンドリーです。独自例外にエラーリストを持たせることで、複数のエラー情報を一度に伝えられます。
問題10:ATMシミュレーション(例外処理の総合問題)⭐⭐⭐
問題
例外処理を活用したATMシミュレーションを作成してください。
独自例外:
-
InsufficientFundsException:残高不足 -
InvalidAmountException:不正な金額 -
AccountLockedException:口座ロック
仕様:
- 暗証番号を3回間違えると口座がロック
- 入金・出金は1000円単位
- 出金は残高以内
期待する出力
=== ATMシミュレーション ===
--- 正常な取引 ---
入金: 50000円 → 残高: 50000円
出金: 20000円 → 残高: 30000円
--- 不正な金額 ---
入金エラー: 金額は1000円単位で指定してください(入力: 1500円)
出金エラー: 金額は正の値で指定してください(入力: -1000円)
--- 残高不足 ---
出金エラー: 残高不足です(残高: 30000円、出金額: 50000円)
--- 口座ロック ---
暗証番号が違います(残り2回)
暗証番号が違います(残り1回)
口座がロックされました
口座ロックエラー: 口座がロックされています。窓口にお問い合わせください
--- 最終残高 ---
残高: 30000円
模範解答
class InsufficientFundsException extends Exception {
private int balance;
private int amount;
InsufficientFundsException(int balance, int amount) {
super("残高不足です(残高: " + balance + "円、出金額: " + amount + "円)");
this.balance = balance;
this.amount = amount;
}
}
class InvalidAmountException extends Exception {
InvalidAmountException(String message) {
super(message);
}
}
class AccountLockedException extends Exception {
AccountLockedException() {
super("口座がロックされています。窓口にお問い合わせください");
}
}
class ATM {
private int balance;
private String pin;
private int failedAttempts;
private boolean locked;
ATM(String pin) {
this.balance = 0;
this.pin = pin;
this.failedAttempts = 0;
this.locked = false;
}
void authenticate(String inputPin) throws AccountLockedException {
if (locked) {
throw new AccountLockedException();
}
if (!pin.equals(inputPin)) {
failedAttempts++;
int remaining = 3 - failedAttempts;
if (remaining <= 0) {
locked = true;
System.out.println("口座がロックされました");
throw new AccountLockedException();
}
System.out.println("暗証番号が違います(残り" + remaining + "回)");
return;
}
failedAttempts = 0;
}
void deposit(int amount) throws InvalidAmountException, AccountLockedException {
if (locked) throw new AccountLockedException();
if (amount <= 0) {
throw new InvalidAmountException("金額は正の値で指定してください(入力: " + amount + "円)");
}
if (amount % 1000 != 0) {
throw new InvalidAmountException("金額は1000円単位で指定してください(入力: " + amount + "円)");
}
balance += amount;
System.out.println("入金: " + amount + "円 → 残高: " + balance + "円");
}
void withdraw(int amount) throws InsufficientFundsException, InvalidAmountException, AccountLockedException {
if (locked) throw new AccountLockedException();
if (amount <= 0) {
throw new InvalidAmountException("金額は正の値で指定してください(入力: " + amount + "円)");
}
if (amount % 1000 != 0) {
throw new InvalidAmountException("金額は1000円単位で指定してください(入力: " + amount + "円)");
}
if (amount > balance) {
throw new InsufficientFundsException(balance, amount);
}
balance -= amount;
System.out.println("出金: " + amount + "円 → 残高: " + balance + "円");
}
int getBalance() {
return balance;
}
}
public class Exercise10 {
public static void main(String[] args) {
ATM atm = new ATM("1234");
System.out.println("=== ATMシミュレーション ===");
// 正常な取引
System.out.println("\n--- 正常な取引 ---");
try {
atm.deposit(50000);
atm.withdraw(20000);
} catch (Exception e) {
System.out.println("エラー: " + e.getMessage());
}
// 不正な金額
System.out.println("\n--- 不正な金額 ---");
try {
atm.deposit(1500);
} catch (InvalidAmountException e) {
System.out.println("入金エラー: " + e.getMessage());
} catch (AccountLockedException e) {
System.out.println("口座ロックエラー: " + e.getMessage());
}
try {
atm.withdraw(-1000);
} catch (InvalidAmountException e) {
System.out.println("出金エラー: " + e.getMessage());
} catch (InsufficientFundsException e) {
System.out.println("出金エラー: " + e.getMessage());
} catch (AccountLockedException e) {
System.out.println("口座ロックエラー: " + e.getMessage());
}
// 残高不足
System.out.println("\n--- 残高不足 ---");
try {
atm.withdraw(50000);
} catch (InsufficientFundsException e) {
System.out.println("出金エラー: " + e.getMessage());
} catch (InvalidAmountException e) {
System.out.println("出金エラー: " + e.getMessage());
} catch (AccountLockedException e) {
System.out.println("口座ロックエラー: " + e.getMessage());
}
// 口座ロック
System.out.println("\n--- 口座ロック ---");
try {
atm.authenticate("0000");
atm.authenticate("0000");
atm.authenticate("0000");
} catch (AccountLockedException e) {
// ロックされた
}
try {
atm.deposit(10000);
} catch (InvalidAmountException e) {
System.out.println("入金エラー: " + e.getMessage());
} catch (AccountLockedException e) {
System.out.println("口座ロックエラー: " + e.getMessage());
}
System.out.println("\n--- 最終残高 ---");
System.out.println("残高: " + atm.getBalance() + "円");
}
}
ポイント: 実務に近い例外処理の総合問題です。状況に応じて異なる独自例外を投げ分け、呼び出し側で適切に処理しています。入力値のバリデーション、業務ロジックのエラー、セキュリティ関連のエラーを例外で整理することで、堅牢なプログラムになります。
まとめ
| 問題 | テーマ | 難易度 |
|---|---|---|
| 問題1 | 基本的な try-catch | ⭐ |
| 問題2 | finally ブロック | ⭐ |
| 問題3 | 複数の例外をキャッチ | ⭐ |
| 問題4 | 例外の伝播(throws) | ⭐⭐ |
| 問題5 | 独自例外クラス | ⭐⭐ |
| 問題6 | try-with-resources | ⭐⭐ |
| 問題7 | 例外チェーン | ⭐⭐ |
| 問題8 | 検査例外と非検査例外 | ⭐⭐ |
| 問題9 | バリデーションフレームワーク | ⭐⭐⭐ |
| 問題10 | ATMシミュレーション(総合問題) | ⭐⭐⭐ |
次回は最終回 コレクションと Stream API のコーディング問題です!
シリーズ一覧:Java研修コーディング問題集
- 変数・データ型・演算子 編
- 条件分岐(if / switch)編
- 繰り返し処理(for / while)編
- 配列 編
- メソッド 編
- 文字列操作(String)編
- クラスとオブジェクト 編
- 継承とインターフェース 編
- 👉 例外処理 編(本記事)
- コレクションと Stream API 編
著者: @kotaro_ai_lab
AI駆動開発やテック情報を毎日発信しています。フォローお気軽にどうぞ!