Javaのカスタム例外
2025-08-02はじめに
カスタム例外とは、プログラマが独自に定義する例外クラスのことです。Javaにはあらかじめ多くの例外クラスが用意されていますが、特定のアプリケーションやビジネスロジックに特化した例外を作成したい場合に使用します。
例外処理を学ぶことで、予期せぬエラーに適切に対処できる堅牢なプログラムを作成でき、クラッシュを防いでユーザーフレンドリーな体験を提供できるほか、デバッグやトラブルシューティングが容易になり、ファイルやデータベース接続などのリソース管理も安全に行えるようになります。
標準例外とカスタム例外の違い
標準例外(組み込み例外)
Javaに最初から備わっている例外クラスです。
NullPointerException:null参照時の例外IllegalArgumentException:不正な引数時の例外IOException:入出力操作時の例外
// 標準例外の使用例
public void setAge(int age) {
if (age < 0) {
throw new IllegalArgumentException("年齢は0以上でなければなりません");
}
this.age = age;
}
このコードは、標準例外 IllegalArgumentException を使用して、不正な引数(負の年齢)を検出・防止する例です。setAge メソッドでは、年齢が0未満の場合に例外をスローし、無効な値がオブジェクトに設定されるのを防ぎます。
カスタム例外(独自例外)
カスタム例外とは、アプリケーション独自のエラー状況を表すために開発者が定義する例外クラスのことです。標準例外では表現しきれない特定のエラー(例:残高不足、データ不整合など)を明確に区別でき、より読みやすく保守性の高いエラーハンドリングを実現します。
次のコードは、銀行口座などの残高不足を表すためのカスタム検査例外(Checked Exception) InsufficientBalanceException を定義しています。
// カスタム例外の定義例
public class InsufficientBalanceException extends Exception {
private double currentBalance;
private double requiredAmount;
public InsufficientBalanceException(double currentBalance, double requiredAmount) {
super("残高不足: 現在の残高 " + currentBalance + "円, 必要金額 " + requiredAmount + "円");
this.currentBalance = currentBalance;
this.requiredAmount = requiredAmount;
}
public double getCurrentBalance() { return currentBalance; }
public double getRequiredAmount() { return requiredAmount; }
}
Exception クラスを継承しており、発生時にはメソッドで throws 宣言を行うか、try-catch で明示的に処理する必要があります。この例外は、現在の残高 (currentBalance) と必要な金額 (requiredAmount) を保持し、エラーメッセージには具体的な金額情報を含めることで、原因の特定やユーザーへの通知をわかりやすくしています。
カスタム例外の作成方法
1. 検査例外(チェック例外)の作成
処理の強制が必要な例外。Exceptionクラスを継承します。このコードは、**検査例外(Checked Exception)**として定義されたカスタム例外 UserNotFoundException の例です。Exception を継承しているため、コンパイラがこの例外をチェックし、メソッドでスローする場合は throws 宣言を行うか、try-catch ブロックで処理する必要があります。
この例外は、指定されたユーザーIDが見つからなかった場合に発生し、userId フィールドを通して問題の特定を容易にします。外部リソース(データベース・APIなど)へのアクセス時に発生する可能性があるエラーを安全に扱うための、明示的なエラーハンドリングを促す設計が特徴です。
// 検査例外の例 - コンパイラがチェックする
public class UserNotFoundException extends Exception {
private String userId;
public UserNotFoundException(String userId) {
super("ユーザーID '" + userId + "' は見つかりません");
this.userId = userId;
}
public String getUserId() { return userId; }
}
2. 非検査例外(ランタイム例外)の作成
処理の強制が不要な例外。RuntimeExceptionクラスを継承します。このコードは、**非検査例外(Unchecked Exception)**として定義されたカスタム例外 InvalidEmailFormatException の例です。RuntimeException を継承しているため、コンパイラによる例外処理の強制がなく、throws 宣言や try-catch を記述しなくてもスローできます。
この例外は、無効なメールアドレス形式が検出された際に発生し、エラーメッセージに不正なアドレス文字列を含めることで、原因の特定やデバッグを容易にしています。主にユーザー入力の検証エラーなど、プログラムのバグやデータ不正に起因するランタイムエラーに対して使用されます。
// 非検査例外の例 - コンパイラがチェックしない
public class InvalidEmailFormatException extends RuntimeException {
private String invalidEmail;
public InvalidEmailFormatException(String invalidEmail) {
super("無効なメールアドレス形式: " + invalidEmail);
this.invalidEmail = invalidEmail;
}
public String getInvalidEmail() { return invalidEmail; }
}
カスタム例外の活用方法
1. ビジネスルールの表現
アプリケーション固有のエラー条件を明確に表現できます。次のコードは、銀行口座の出金処理において残高不足を検出して例外で通知する仕組みを示しています。
BankAccount クラスの withdraw メソッドでは、引き出し金額が残高を上回る場合に InsufficientBalanceException(カスタム例外)をスローし、異常を明示的に伝えます。BankService クラスでは try-catch を用いてこの例外を捕捉し、ユーザーにエラーメッセージと不足金額を表示します。
これにより、ビジネスロジックとエラーハンドリングを分離し、安全で明確な取引処理を実現しています。
public class BankAccount {
private double balance;
public void withdraw(double amount) throws InsufficientBalanceException {
if (amount > balance) {
throw new InsufficientBalanceException(balance, amount);
}
balance -= amount;
}
}
// 使用例
public class BankService {
public void processWithdrawal(BankAccount account, double amount) {
try {
account.withdraw(amount);
System.out.println("出金成功");
} catch (InsufficientBalanceException e) {
System.out.println("エラー: " + e.getMessage());
System.out.println("不足金額: " + (e.getRequiredAmount() - e.getCurrentBalance()));
}
}
}
2. 例外の分類と階層化
関連する例外をグループ化して管理できます。次のコードは、支払い処理に関するエラーを整理して扱うためのカスタム例外クラス階層を定義しています。
PaymentException は共通の親クラスとして抽象化されており、すべての支払い関連例外の基盤となります。CardDeclinedException はカード決済が拒否された場合、InvalidCurrencyException は無効な通貨が指定された場合にスローされる具体的な例外クラスです。
このように例外を階層化することで、個別のエラーに応じた処理が可能になると同時に、PaymentException 1つで支払い全体の例外を一括処理する柔軟な設計が実現できます。
// 基本となるカスタム例外
public abstract class PaymentException extends Exception {
public PaymentException(String message) {
super(message);
}
}
// 特定の例外タイプ
public class CardDeclinedException extends PaymentException {
public CardDeclinedException(String cardNumber) {
super("カードが拒否されました: " + cardNumber);
}
}
public class InvalidCurrencyException extends PaymentException {
public InvalidCurrencyException(String currency) {
super("無効な通貨: " + currency);
}
}
3. 詳細なエラー情報の伝達
例外オブジェクトに追加情報を持たせることができます。次のコードは、入力データの検証エラーを表す**カスタム例外クラス ValidationException**の定義です。
Exception を継承しており、特定のフィールド名 (fieldName)、適用された検証ルール (validationRule)、および不正な値 (invalidValue) を保持します。コンストラクタでは、どのフィールドがどのルールに違反したかを明確に示すエラーメッセージを生成し、デバッグやログ出力時に原因を特定しやすくしています。
これにより、フォーム入力やデータ整合性チェックなどの場面で、詳細な検証エラー情報を安全かつ効率的に扱うことができます。
public class ValidationException extends Exception {
private String fieldName;
private String validationRule;
private Object invalidValue;
public ValidationException(String fieldName, String validationRule, Object invalidValue) {
super("フィールド '" + fieldName + "' の検証失敗: " + validationRule);
this.fieldName = fieldName;
this.validationRule = validationRule;
this.invalidValue = invalidValue;
}
// ゲッターメソッド
public String getFieldName() { return fieldName; }
public String getValidationRule() { return validationRule; }
public Object getInvalidValue() { return invalidValue; }
}
カスタム例外を使用するメリット
1. コードの可読性向上
例外の名前からエラーの内容が明確にわかります。次のコードは、在庫が不足している状況を例外で通知する2つの方法を示しています。最初の行では、Java標準の IllegalArgumentException を使用して単純にエラーメッセージを伝えています。
一方、2行目では InsufficientStockException というカスタム例外を用い、商品IDや要求数量、現在の在庫数といった具体的な情報を含めて、より詳細で状況に応じたエラー処理を実現しています。
// 標準例外のみ使用
throw new IllegalArgumentException("在庫数が不足しています");
// カスタム例外使用
throw new InsufficientStockException(productId, requestedQuantity, currentStock);
2. 特定の例外処理が容易
特定の例外タイプに応じた処理を簡単に記述できます。次のコードは、支払い処理中に発生するさまざまな種類の例外を個別に処理する例です。paymentProcessor.processPayment(order) の実行中に例外が発生すると、まず CardDeclinedException(カード拒否)を検知してユーザー通知を行い、次に InsufficientFundsException(残高不足)を捕捉して代替支払い方法の提案を行います。
それ以外の支払い関連の例外は PaymentException としてまとめて処理され、一般的なエラー対応を行うよう設計されています。このように複数の catch ブロックを使うことで、例外の種類ごとに適切な対応を行う柔軟なエラーハンドリングを実現しています。
try {
paymentProcessor.processPayment(order);
} catch (CardDeclinedException e) {
// カード拒否時の特別な処理
notifyUserOfCardDecline(e);
} catch (InsufficientFundsException e) {
// 残高不足時の処理
suggestAlternativePayment(e);
} catch (PaymentException e) {
// その他の支払い例外
handleGenericPaymentError(e);
}
3. 保守性の向上
エラーハンドリングのロジックが一元化され、変更が容易になります。
例外クラスを設計する際のベストプラクティスとしては、まず例外の内容が直感的に分かるような意味のある名前を付けることが重要です。また、例外の性質に応じて検査例外か非検査例外かを判断し、適切なスーパークラス(Exception または RuntimeException)を選択します。
さらに、エラーの原因を特定しやすくするために有用な情報を例外メッセージやフィールドとして保持することが望まれます。併せて、その例外がどのような条件で発生し、どのような意味を持つのかをドキュメント化しておくことも大切です。最後に、例外の定義は必要最低限にとどめ、過剰なカスタム例外の作成を避けることでコードの保守性と可読性を保つことが推奨されます。
スタックトレース無効化の例
このコードは、スタックトレースの生成を省略して処理負荷を軽減するカスタム例外 LightweightException の定義例です。RuntimeException を継承しており、fillInStackTrace() メソッドをオーバーライドして何も行わないようにすることで、例外生成時のコストを削減しています。
public class LightweightException extends RuntimeException {
@Override
public synchronized Throwable fillInStackTrace() {
return this; // スタックトレースを生成しない
}
public LightweightException(String message) {
super(message);
}
}
大量発生が想定される軽微な例外(例:制御フロー目的の例外など)に適しており、パフォーマンス重視のシステムで例外処理のオーバーヘッドを抑える際に有効です。
まとめ
カスタム例外は、アプリケーションのエラーハンドリングをより精密で意味のあるものにします。標準例外では表現しきれないドメイン固有のエラー条件を表現でき、コードの可読性と保守性を大幅に向上させることができます。初学者はまず標準例外の使い方をマスターした上で、プロジェクトの必要性に応じてカスタム例外を導入していくことをおすすめします。
演習問題
初級問題 (3問)
問題1: 基本的なカスタム例外の作成
銀行口座の残高不足を表すカスタム例外InsufficientBalanceExceptionを作成してください。この例外は以下の要件を満たすこと。
Exceptionクラスを継承すること- 現在の残高と必要な金額をコンストラクタで受け取ること
- 適切なエラーメッセージを表示すること
// ここにInsufficientBalanceExceptionクラスを実装してください
public class BankAccount {
private double balance;
public void withdraw(double amount) throws InsufficientBalanceException {
// 残高不足の場合にカスタム例外をスローする処理を実装
}
}
問題2: ランタイムカスタム例外の作成
ユーザー登録時のメールアドレス形式不正を表すInvalidEmailExceptionを作成してください。この例外は以下の要件を満たすこと。
RuntimeExceptionクラスを継承すること- 不正なメールアドレスを保持すること
- 検査例外ではなく非検査例外とすること
// ここにInvalidEmailExceptionクラスを実装してください
public class UserValidator {
public void validateEmail(String email) {
// メールアドレスのバリデーションを行い、不正な場合に例外をスロー
}
}
問題3: 例外階層の基本
在庫管理システム用の例外階層を作成してください。
- 基本例外クラス
InventoryException(Exception継承) - 在庫不足例外
OutOfStockException(InventoryException継承) - 商品未検出例外
ProductNotFoundException(InventoryException継承)
中級問題 (6問)
問題4: 詳細情報を持つカスタム例外
注文処理システム用のOrderProcessingExceptionを作成してください。以下の情報を保持すること。
- 注文ID
- エラーコード
- タイムスタンプ
- 元の例外(原因)
// ここにOrderProcessingExceptionクラスを実装してください
public class OrderProcessor {
public void processOrder(String orderId) throws OrderProcessingException {
// 注文処理のシミュレーション
}
}
問題5: 例外チェーニングの実装
ファイル処理システムで、低レベルのIO例外をアプリケーション固有の例外でラップする実装を作成してください。
// カスタム例外を定義
public class FileProcessingException extends Exception {
// 適切なコンストラクタを実装
}
public class FileManager {
public void processFile(String filename) throws FileProcessingException {
try {
// ファイル処理のシミュレーション
if (filename == null) {
throw new IOException("ファイル名がnullです");
}
} catch (IOException e) {
// カスタム例外でラップしてスロー
}
}
}
問題6: 複数例外の統合
ECサイトの決済システムで、複数のエラー条件を単一のカスタム例外で処理する実装を作成してください。
public class PaymentException extends Exception {
private PaymentErrorType errorType;
private String transactionId;
public enum PaymentErrorType {
INSUFFICIENT_FUNDS, INVALID_CARD, NETWORK_ERROR, TIMEOUT
}
// コンストラクタとゲッターを実装
}
public class PaymentProcessor {
public void processPayment(double amount, String cardNumber) throws PaymentException {
// 各種エラー条件をチェックし、適切なPaymentExceptionをスロー
}
}
問題7: バリデーション例外の作成
フォームバリデーション用のカスタム例外を作成し、複数のバリデーションエラーをまとめて報告できるようにしてください。
public class ValidationException extends Exception {
private List errorMessages;
private String fieldName;
// 複数のエラーメッセージを保持できるコンストラクタを実装
// エラーメッセージ一覧を取得するメソッドを実装
}
public class UserRegistration {
public void validateUser(String username, String email, int age) throws ValidationException {
List errors = new ArrayList<>();
// 各種バリデーションを実行し、エラーをリストに追加
// エラーがある場合にValidationExceptionをスロー
}
}
問題8: リトライ可能例外の設計
ネットワーク操作などでリトライ可能な例外とリトライ不可な例外を区別する例外階層を作成してください。
// 基本例外
public abstract class NetworkException extends Exception {
private final boolean retryable;
// コンストラクタとゲッターを実装
}
// リトライ可能な例外
public class RetryableNetworkException extends NetworkException {
// 実装
}
// リトライ不可な例外
public class FatalNetworkException extends NetworkException {
// 実装
}
public class NetworkService {
public void sendRequest() throws NetworkException {
// ネットワークエラーをシミュレートし、適切な例外をスロー
}
}
問題9: 業務ドメイン例外の設計
航空予約システム用の例外群を設計してください。以下の例外を含むこと。
FlightFullException(便満席)SeatAlreadyTakenException(座席重複予約)InvalidBookingPeriodException(無効な予約期間)
// 例外クラス群を実装してください
public class FlightBookingSystem {
public void bookFlight(String flightNumber, String seatNumber, LocalDate date)
throws FlightFullException, SeatAlreadyTakenException, InvalidBookingPeriodException {
// 予約処理を実装
}
}
上級問題 (3問)
問題10: 国際化対応例外の作成
多言語対応アプリケーション向けに、メッセージリソースを使用した国際化対応のカスタム例外を作成してください。
public class LocalizedException extends Exception {
private final String messageKey;
private final Object[] messageArgs;
// リソースバンドルを使用してローカライズされたメッセージを取得する実装
// デフォルトロケールと指定ロケール両方に対応
}
public class InternationalService {
public void performOperation() throws LocalizedException {
// エラー発生時にローカライズされた例外をスロー
}
}
問題11: 例外コンテキスト情報の保持
複雑な業務処理で、例外発生時のコンテキスト情報(ユーザーID、セッションID、操作名など)を保持する高度なカスタム例外を作成してください。
public class BusinessException extends Exception {
private final String userId;
private final String sessionId;
private final String operationName;
private final Map contextData;
private final Instant timestamp;
// ビルダーパターンを使用したコンストラクタを実装
// コンテキスト情報を追加するメソッドを実装
public static class Builder {
// ビルダークラスの実装
}
}
public class ComplexBusinessService {
public void executeBusinessProcess(String userId, String sessionId) throws BusinessException {
// 業務処理を実行し、エラー時に詳細なコンテキスト情報を含む例外をスロー
}
}
問題12: 例外ハンドリングフレームワークの設計
カスタム例外を使用したエラーハンドリングフレームワークを設計してください。以下の機能を含むこと。
- 例外とHTTPステータスコードのマッピング
- 例外ロギングの自動化
- ユーザーフレンドリーなエラーレスポンス生成
public abstract class ApiException extends RuntimeException {
private final int httpStatusCode;
private final String errorCode;
// コンストラクタとゲッターを実装
}
public class NotFoundException extends ApiException {
// 実装
}
public class ValidationException extends ApiException {
// 実装
}
public class AuthenticationException extends ApiException {
// 実装
}
public class ExceptionHandlerFramework {
public void handleException(ApiException e, HttpServletResponse response) {
// 例外を適切なHTTPレスポンスに変換する処理
// ロギング、エラーレスポンス生成などを実装
}
}
演習問題 解答例
初級問題 解答例
問題1: 基本的なカスタム例外の作成
// InsufficientBalanceExceptionクラスの実装
public class InsufficientBalanceException extends Exception {
private double currentBalance;
private double requiredAmount;
public InsufficientBalanceException(double currentBalance, double requiredAmount) {
super("残高不足: 現在の残高 " + currentBalance + "円, 必要金額 " + requiredAmount + "円");
this.currentBalance = currentBalance;
this.requiredAmount = requiredAmount;
}
public double getCurrentBalance() {
return currentBalance;
}
public double getRequiredAmount() {
return requiredAmount;
}
}
public class BankAccount {
private double balance;
public void withdraw(double amount) throws InsufficientBalanceException {
if (amount > balance) {
throw new InsufficientBalanceException(balance, amount);
}
balance -= amount;
}
public double getBalance() {
return balance;
}
public void deposit(double amount) {
balance += amount;
}
}
問題2: ランタイムカスタム例外の作成
// InvalidEmailExceptionクラスの実装
public class InvalidEmailException extends RuntimeException {
private String invalidEmail;
public InvalidEmailException(String invalidEmail) {
super("無効なメールアドレス形式: " + invalidEmail);
this.invalidEmail = invalidEmail;
}
public String getInvalidEmail() {
return invalidEmail;
}
}
public class UserValidator {
public void validateEmail(String email) {
if (email == null || !email.contains("@") || !email.contains(".")) {
throw new InvalidEmailException(email);
}
// その他のバリデーションルール
if (email.length() < 5) {
throw new InvalidEmailException(email);
}
}
public static void main(String[] args) {
UserValidator validator = new UserValidator();
try {
validator.validateEmail("invalid-email");
} catch (InvalidEmailException e) {
System.out.println("エラー: " + e.getMessage());
}
}
}
問題3: 例外階層の基本
// 例外クラス群の実装
public class InventoryException extends Exception {
public InventoryException(String message) {
super(message);
}
public InventoryException(String message, Throwable cause) {
super(message, cause);
}
}
public class OutOfStockException extends InventoryException {
private String productId;
private int requestedQuantity;
private int availableQuantity;
public OutOfStockException(String productId, int requestedQuantity, int availableQuantity) {
super("在庫不足: 商品ID " + productId + " の要求数量 " + requestedQuantity +
" に対して在庫数量 " + availableQuantity + " です");
this.productId = productId;
this.requestedQuantity = requestedQuantity;
this.availableQuantity = availableQuantity;
}
public String getProductId() { return productId; }
public int getRequestedQuantity() { return requestedQuantity; }
public int getAvailableQuantity() { return availableQuantity; }
}
public class ProductNotFoundException extends InventoryException {
private String productId;
public ProductNotFoundException(String productId) {
super("商品未検出: 商品ID " + productId + " は存在しません");
this.productId = productId;
}
public String getProductId() {
return productId;
}
}
中級問題 解答例
問題4: 詳細情報を持つカスタム例外
// OrderProcessingExceptionクラスの実装
import java.time.LocalDateTime;
public class OrderProcessingException extends Exception {
private String orderId;
private String errorCode;
private LocalDateTime timestamp;
public OrderProcessingException(String orderId, String errorCode, String message) {
super(message);
this.orderId = orderId;
this.errorCode = errorCode;
this.timestamp = LocalDateTime.now();
}
public OrderProcessingException(String orderId, String errorCode, String message, Throwable cause) {
super(message, cause);
this.orderId = orderId;
this.errorCode = errorCode;
this.timestamp = LocalDateTime.now();
}
// ゲッターメソッド
public String getOrderId() { return orderId; }
public String getErrorCode() { return errorCode; }
public LocalDateTime getTimestamp() { return timestamp; }
@Override
public String getMessage() {
return String.format("[%s] Order: %s, Code: %s - %s",
timestamp, orderId, errorCode, super.getMessage());
}
}
public class OrderProcessor {
public void processOrder(String orderId) throws OrderProcessingException {
try {
// 注文処理のシミュレーション
if (orderId == null || orderId.isEmpty()) {
throw new OrderProcessingException(orderId, "INVALID_ORDER_ID",
"注文IDが無効です");
}
// 在庫チェックのシミュレーション
if (orderId.startsWith("OUT")) {
throw new OrderProcessingException(orderId, "OUT_OF_STOCK",
"商品在庫が不足しています");
}
System.out.println("注文 " + orderId + " を処理しました");
} catch (OrderProcessingException e) {
throw e;
} catch (Exception e) {
throw new OrderProcessingException(orderId, "PROCESSING_ERROR",
"注文処理中にエラーが発生しました", e);
}
}
}
問題5: 例外チェーニングの実装
// カスタム例外の定義
public class FileProcessingException extends Exception {
private String filename;
public FileProcessingException(String filename, String message) {
super(message);
this.filename = filename;
}
public FileProcessingException(String filename, String message, Throwable cause) {
super(message, cause);
this.filename = filename;
}
public String getFilename() {
return filename;
}
@Override
public String getMessage() {
return "ファイル '" + filename + "' の処理中にエラー: " + super.getMessage();
}
}
public class FileManager {
public void processFile(String filename) throws FileProcessingException {
try {
// ファイル処理のシミュレーション
if (filename == null) {
throw new java.io.IOException("ファイル名がnullです");
}
if (!filename.endsWith(".txt")) {
throw new java.io.IOException("サポートされていないファイル形式です");
}
System.out.println("ファイル " + filename + " を処理しました");
} catch (java.io.IOException e) {
// カスタム例外でラップしてスロー
throw new FileProcessingException(filename,
"ファイル入出力エラーが発生しました", e);
}
}
}
問題6: 複数例外の統合
public class PaymentException extends Exception {
private PaymentErrorType errorType;
private String transactionId;
public enum PaymentErrorType {
INSUFFICIENT_FUNDS,
INVALID_CARD,
NETWORK_ERROR,
TIMEOUT
}
public PaymentException(PaymentErrorType errorType, String transactionId, String message) {
super(message);
this.errorType = errorType;
this.transactionId = transactionId;
}
public PaymentException(PaymentErrorType errorType, String transactionId,
String message, Throwable cause) {
super(message, cause);
this.errorType = errorType;
this.transactionId = transactionId;
}
// ゲッターメソッド
public PaymentErrorType getErrorType() { return errorType; }
public String getTransactionId() { return transactionId; }
@Override
public String getMessage() {
return String.format("[%s] Transaction: %s - %s",
errorType, transactionId, super.getMessage());
}
}
public class PaymentProcessor {
public void processPayment(double amount, String cardNumber) throws PaymentException {
// カード番号のバリデーション
if (cardNumber == null || cardNumber.length() != 16) {
throw new PaymentException(PaymentException.PaymentErrorType.INVALID_CARD,
"TXN001", "無効なカード番号です: " + cardNumber);
}
// 残高チェックのシミュレーション
if (amount > 1000) {
throw new PaymentException(PaymentException.PaymentErrorType.INSUFFICIENT_FUNDS,
"TXN001", "残高不足: 要求金額 " + amount);
}
// ネットワークエラーのシミュレーション
if (Math.random() > 0.7) {
throw new PaymentException(PaymentException.PaymentErrorType.NETWORK_ERROR,
"TXN001", "決済ネットワークに接続できません");
}
System.out.println("決済成功: " + amount + "円");
}
}
問題7: バリデーション例外の作成
import java.util.ArrayList;
import java.util.List;
public class ValidationException extends Exception {
private List errorMessages;
private String fieldName;
public ValidationException(String fieldName, String errorMessage) {
super(errorMessage);
this.fieldName = fieldName;
this.errorMessages = new ArrayList<>();
this.errorMessages.add(errorMessage);
}
public ValidationException(List errorMessages) {
super(String.join(", ", errorMessages));
this.errorMessages = new ArrayList<>(errorMessages);
this.fieldName = "multiple";
}
// ゲッターメソッド
public List getErrorMessages() { return errorMessages; }
public String getFieldName() { return fieldName; }
public void addErrorMessage(String errorMessage) {
this.errorMessages.add(errorMessage);
}
@Override
public String getMessage() {
if (errorMessages.size() == 1) {
return "バリデーションエラー (" + fieldName + "): " + super.getMessage();
} else {
return "複数のバリデーションエラー: " + String.join(", ", errorMessages);
}
}
}
public class UserRegistration {
public void validateUser(String username, String email, int age) throws ValidationException {
List errors = new ArrayList<>();
// ユーザー名のバリデーション
if (username == null || username.length() < 3) {
errors.add("ユーザー名は3文字以上である必要があります");
}
// メールアドレスのバリデーション
if (email == null || !email.contains("@")) {
errors.add("有効なメールアドレスを入力してください");
}
// 年齢のバリデーション
if (age < 0 || age > 150) {
errors.add("年齢は0以上150以下である必要があります");
}
// エラーがある場合にValidationExceptionをスロー
if (!errors.isEmpty()) {
throw new ValidationException(errors);
}
System.out.println("バリデーション成功");
}
}
問題8: リトライ可能例外の設計
// 基本例外
public abstract class NetworkException extends Exception {
private final boolean retryable;
public NetworkException(String message, boolean retryable) {
super(message);
this.retryable = retryable;
}
public NetworkException(String message, boolean retryable, Throwable cause) {
super(message, cause);
this.retryable = retryable;
}
public boolean isRetryable() {
return retryable;
}
}
// リトライ可能な例外
public class RetryableNetworkException extends NetworkException {
public RetryableNetworkException(String message) {
super(message, true);
}
public RetryableNetworkException(String message, Throwable cause) {
super(message, true, cause);
}
}
// リトライ不可な例外
public class FatalNetworkException extends NetworkException {
public FatalNetworkException(String message) {
super(message, false);
}
public FatalNetworkException(String message, Throwable cause) {
super(message, false, cause);
}
}
public class NetworkService {
private int attemptCount = 0;
public void sendRequest() throws NetworkException {
attemptCount++;
// ネットワークエラーをシミュレート
double random = Math.random();
if (random < 0.4) {
// 一時的なネットワークエラー(リトライ可能)
throw new RetryableNetworkException("一時的なネットワークエラーが発生しました");
} else if (random < 0.7) {
// 認証エラー(リトライ不可)
throw new FatalNetworkException("認証に失敗しました");
} else if (random < 0.9) {
// タイムアウト(リトライ可能)
throw new RetryableNetworkException("リクエストがタイムアウトしました");
}
System.out.println("リクエスト成功: 試行回数 " + attemptCount);
attemptCount = 0; // リセット
}
public void sendRequestWithRetry(int maxRetries) throws NetworkException {
for (int i = 0; i < maxRetries; i++) {
try {
sendRequest();
return; // 成功したら終了
} catch (RetryableNetworkException e) {
System.out.println("リトライ可能なエラー: " + e.getMessage() +
" (試行 " + (i + 1) + "/" + maxRetries + ")");
if (i == maxRetries - 1) {
throw e; // 最終試行で失敗
}
} catch (FatalNetworkException e) {
System.out.println("リトライ不可なエラー: " + e.getMessage());
throw e; // 即時終了
}
}
}
}
問題9: 業務ドメイン例外の設計
import java.time.LocalDate;
// 基本例外クラス
public class FlightBookingException extends Exception {
public FlightBookingException(String message) {
super(message);
}
}
// 便満席例外
public class FlightFullException extends FlightBookingException {
private String flightNumber;
private LocalDate date;
public FlightFullException(String flightNumber, LocalDate date) {
super("便 " + flightNumber + " は " + date + " の時点で満席です");
this.flightNumber = flightNumber;
this.date = date;
}
public String getFlightNumber() { return flightNumber; }
public LocalDate getDate() { return date; }
}
// 座席重複予約例外
public class SeatAlreadyTakenException extends FlightBookingException {
private String flightNumber;
private String seatNumber;
public SeatAlreadyTakenException(String flightNumber, String seatNumber) {
super("便 " + flightNumber + " の座席 " + seatNumber + " は既に予約済みです");
this.flightNumber = flightNumber;
this.seatNumber = seatNumber;
}
public String getFlightNumber() { return flightNumber; }
public String getSeatNumber() { return seatNumber; }
}
// 無効な予約期間例外
public class InvalidBookingPeriodException extends FlightBookingException {
private LocalDate bookingDate;
private LocalDate flightDate;
public InvalidBookingPeriodException(LocalDate bookingDate, LocalDate flightDate) {
super("予約日 " + bookingDate + " から便日 " + flightDate + " までの期間が無効です");
this.bookingDate = bookingDate;
this.flightDate = flightDate;
}
public LocalDate getBookingDate() { return bookingDate; }
public LocalDate getFlightDate() { return flightDate; }
}
public class FlightBookingSystem {
public void bookFlight(String flightNumber, String seatNumber, LocalDate date)
throws FlightFullException, SeatAlreadyTakenException, InvalidBookingPeriodException {
// 予約期間のチェック
LocalDate today = LocalDate.now();
if (date.isBefore(today.plusDays(1))) {
throw new InvalidBookingPeriodException(today, date);
}
// 便満席チェックのシミュレーション
if (flightNumber.endsWith("FULL")) {
throw new FlightFullException(flightNumber, date);
}
// 座席重複チェックのシミュレーション
if (seatNumber.equals("1A") || seatNumber.equals("1B")) {
throw new SeatAlreadyTakenException(flightNumber, seatNumber);
}
System.out.println("予約成功: 便 " + flightNumber +
", 座席 " + seatNumber +
", 日付 " + date);
}
}
上級問題 解答例
問題10: 国際化対応例外の作成
import java.util.ResourceBundle;
import java.util.Locale;
public class LocalizedException extends Exception {
private final String messageKey;
private final Object[] messageArgs;
private final Locale locale;
private static final String BUNDLE_NAME = "ErrorMessages";
public LocalizedException(String messageKey, Object... messageArgs) {
this(messageKey, Locale.getDefault(), messageArgs);
}
public LocalizedException(String messageKey, Locale locale, Object... messageArgs) {
super(getLocalizedMessage(messageKey, locale, messageArgs));
this.messageKey = messageKey;
this.locale = locale;
this.messageArgs = messageArgs;
}
public LocalizedException(String messageKey, Throwable cause, Object... messageArgs) {
this(messageKey, Locale.getDefault(), cause, messageArgs);
}
public LocalizedException(String messageKey, Locale locale, Throwable cause, Object... messageArgs) {
super(getLocalizedMessage(messageKey, locale, messageArgs), cause);
this.messageKey = messageKey;
this.locale = locale;
this.messageArgs = messageArgs;
}
private static String getLocalizedMessage(String key, Locale locale, Object... args) {
try {
ResourceBundle bundle = ResourceBundle.getBundle(BUNDLE_NAME, locale);
String pattern = bundle.getString(key);
return String.format(pattern, args);
} catch (Exception e) {
return "エラーメッセージの読み込みに失敗: " + key;
}
}
// ゲッターメソッド
public String getMessageKey() { return messageKey; }
public Locale getLocale() { return locale; }
public Object[] getMessageArgs() { return messageArgs; }
public String getMessage(Locale specificLocale) {
return getLocalizedMessage(messageKey, specificLocale, messageArgs);
}
}
public class InternationalService {
public void performOperation(String userId) throws LocalizedException {
if (userId == null || userId.isEmpty()) {
throw new LocalizedException("error.user.id.required", userId);
}
if (userId.length() < 5) {
throw new LocalizedException("error.user.id.too.short", userId, 5);
}
// 操作成功
System.out.println("操作成功: " + userId);
}
public static void main(String[] args) {
InternationalService service = new InternationalService();
try {
service.performOperation("abc"); // 短すぎるID
} catch (LocalizedException e) {
System.out.println("日本語: " + e.getMessage());
System.out.println("English: " + e.getMessage(Locale.ENGLISH));
}
}
}
問題11: 例外コンテキスト情報の保持
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
public class BusinessException extends Exception {
private final String userId;
private final String sessionId;
private final String operationName;
private final Map contextData;
private final Instant timestamp;
private BusinessException(Builder builder) {
super(builder.message, builder.cause);
this.userId = builder.userId;
this.sessionId = builder.sessionId;
this.operationName = builder.operationName;
this.contextData = new HashMap<>(builder.contextData);
this.timestamp = Instant.now();
}
// ゲッターメソッド
public String getUserId() { return userId; }
public String getSessionId() { return sessionId; }
public String getOperationName() { return operationName; }
public Map getContextData() { return new HashMap<>(contextData); }
public Instant getTimestamp() { return timestamp; }
@Override
public String getMessage() {
return String.format("[%s] User: %s, Operation: %s - %s",
timestamp, userId, operationName, super.getMessage());
}
// ビルダークラス
public static class Builder {
private String message;
private Throwable cause;
private String userId;
private String sessionId;
private String operationName;
private Map contextData = new HashMap<>();
public Builder(String message) {
this.message = message;
}
public Builder withCause(Throwable cause) {
this.cause = cause;
return this;
}
public Builder withUserId(String userId) {
this.userId = userId;
return this;
}
public Builder withSessionId(String sessionId) {
this.sessionId = sessionId;
return this;
}
public Builder withOperationName(String operationName) {
this.operationName = operationName;
return this;
}
public Builder withContext(String key, Object value) {
this.contextData.put(key, value);
return this;
}
public Builder withContext(Map contextData) {
this.contextData.putAll(contextData);
return this;
}
public BusinessException build() {
return new BusinessException(this);
}
}
}
public class ComplexBusinessService {
public void executeBusinessProcess(String userId, String sessionId) throws BusinessException {
Map context = new HashMap<>();
context.put("startTime", Instant.now());
context.put("inputData", "sample data");
try {
// 複雑な業務処理のシミュレーション
if (userId == null) {
throw new BusinessException.Builder("ユーザーIDが指定されていません")
.withUserId(userId)
.withSessionId(sessionId)
.withOperationName("executeBusinessProcess")
.withContext(context)
.build();
}
// 権限チェックのシミュレーション
if (!userId.startsWith("USER")) {
throw new BusinessException.Builder("無効なユーザーID形式です")
.withUserId(userId)
.withSessionId(sessionId)
.withOperationName("executeBusinessProcess")
.withContext("expectedPrefix", "USER")
.withContext("actualUserId", userId)
.build();
}
context.put("endTime", Instant.now());
context.put("status", "success");
System.out.println("業務処理成功: " + userId);
} catch (BusinessException e) {
// 追加のコンテキスト情報を追加
context.put("endTime", Instant.now());
context.put("status", "failed");
context.put("errorType", e.getClass().getSimpleName());
throw new BusinessException.Builder(e.getMessage())
.withCause(e)
.withUserId(userId)
.withSessionId(sessionId)
.withOperationName("executeBusinessProcess")
.withContext(context)
.build();
}
}
}
問題12: 例外ハンドリングフレームワークの設計
import java.util.HashMap;
import java.util.Map;
// 基本API例外クラス
public abstract class ApiException extends RuntimeException {
private final int httpStatusCode;
private final String errorCode;
private final Map details;
public ApiException(int httpStatusCode, String errorCode, String message) {
super(message);
this.httpStatusCode = httpStatusCode;
this.errorCode = errorCode;
this.details = new HashMap<>();
}
public ApiException(int httpStatusCode, String errorCode, String message, Throwable cause) {
super(message, cause);
this.httpStatusCode = httpStatusCode;
this.errorCode = errorCode;
this.details = new HashMap<>();
}
// ゲッターメソッド
public int getHttpStatusCode() { return httpStatusCode; }
public String getErrorCode() { return errorCode; }
public Map getDetails() { return new HashMap<>(details); }
public ApiException withDetail(String key, Object value) {
this.details.put(key, value);
return this;
}
}
// 具体的な例外クラス
public class NotFoundException extends ApiException {
public NotFoundException(String resourceType, String resourceId) {
super(404, "NOT_FOUND",
resourceType + " with ID '" + resourceId + "' was not found");
withDetail("resourceType", resourceType)
.withDetail("resourceId", resourceId);
}
}
public class ValidationException extends ApiException {
public ValidationException(String field, String message) {
super(400, "VALIDATION_ERROR",
"Validation failed for field '" + field + "': " + message);
withDetail("field", field)
.withDetail("validationMessage", message);
}
}
public class AuthenticationException extends ApiException {
public AuthenticationException(String reason) {
super(401, "AUTHENTICATION_FAILED",
"Authentication failed: " + reason);
withDetail("reason", reason);
}
}
// 例外ハンドリングフレームワーク
public class ExceptionHandlerFramework {
private static final Map, Integer> exceptionStatusMap = new HashMap<>();
static {
exceptionStatusMap.put(NotFoundException.class, 404);
exceptionStatusMap.put(ValidationException.class, 400);
exceptionStatusMap.put(AuthenticationException.class, 401);
}
public void handleException(ApiException e, HttpServletResponse response) {
try {
// HTTPステータスコードの設定
int statusCode = e.getHttpStatusCode();
response.setStatus(statusCode);
response.setContentType("application/json");
// エラーレスポンスの構築
Map errorResponse = new HashMap<>();
errorResponse.put("timestamp", java.time.Instant.now().toString());
errorResponse.put("status", statusCode);
errorResponse.put("error", e.getErrorCode());
errorResponse.put("message", e.getMessage());
errorResponse.put("details", e.getDetails());
// スタックトレース(開発環境のみ)
if (isDevelopment()) {
errorResponse.put("stackTrace", getStackTrace(e));
}
// ロギング
logException(e);
// JSONレスポンスの書き出し
String jsonResponse = convertToJson(errorResponse);
response.getWriter().write(jsonResponse);
} catch (Exception handlerException) {
System.err.println("例外ハンドリング中にエラーが発生: " + handlerException.getMessage());
}
}
private void logException(ApiException e) {
// 実際の実装ではロギングフレームワークを使用
System.err.println("API Exception: " + e.getClass().getSimpleName());
System.err.println("Status: " + e.getHttpStatusCode());
System.err.println("Message: " + e.getMessage());
System.err.println("Details: " + e.getDetails());
if (e.getCause() != null) {
System.err.println("Root Cause: " + e.getCause().getMessage());
}
}
private boolean isDevelopment() {
// 環境判定の実装
return true; // 開発環境と仮定
}
private String getStackTrace(Throwable e) {
java.io.StringWriter sw = new java.io.StringWriter();
java.io.PrintWriter pw = new java.io.PrintWriter(sw);
e.printStackTrace(pw);
return sw.toString();
}
private String convertToJson(Map map) {
// 簡易的なJSON変換(実際の実装ではJacksonやGsonを使用)
StringBuilder json = new StringBuilder("{");
boolean first = true;
for (Map.Entry entry : map.entrySet()) {
if (!first) {
json.append(",");
}
json.append("\"").append(entry.getKey()).append("\":");
if (entry.getValue() instanceof String) {
json.append("\"").append(entry.getValue()).append("\"");
} else {
json.append(entry.getValue());
}
first = false;
}
json.append("}");
return json.toString();
}
}
// 疑似HttpServletResponse(実際のWebフレームワークでは提供される)
class HttpServletResponse {
private int status;
private String contentType;
private java.io.PrintWriter writer = new java.io.PrintWriter(System.out);
public void setStatus(int status) { this.status = status; }
public void setContentType(String contentType) { this.contentType = contentType; }
public java.io.PrintWriter getWriter() { return writer; }
}