9-4. 例外処理・null安全・Optionalの実践
このセクションで学ぶこと
Section titled “このセクションで学ぶこと”- 検査例外と非検査例外の違いと使い分けを理解する
- try-catch-finally と try-with-resources の使い方を習得する
- 独自例外クラスの定義と例外の変換パターンを学ぶ
- Optional を実務で正しく使うパターンとアンチパターンを知る
- ガード節(早期リターン)による読みやすいコードの書き方を学ぶ
第4章で例外処理の概念を学んだ。ここではJavaでの具体的な実装パターンを習得する。
例外の種類と階層
Section titled “例外の種類と階層”Throwable├── Error(回復不可能な深刻なエラー)│ ├── OutOfMemoryError│ ├── StackOverflowError│ └── ...└── Exception(プログラムで処理できるエラー) ├── RuntimeException(非検査例外:throws宣言不要) │ ├── NullPointerException │ ├── IllegalArgumentException │ ├── IllegalStateException │ ├── ArrayIndexOutOfBoundsException │ └── ... └── 検査例外(throws宣言またはcatch必須) ├── IOException ├── SQLException └── ...検査例外 vs 非検査例外
Section titled “検査例外 vs 非検査例外”// 検査例外(IOException):コンパイラが catch か throws を強制するtry { FileReader reader = new FileReader("file.txt");} catch (IOException e) { System.err.println("ファイルを開けません: " + e.getMessage());}
// 非検査例外(RuntimeException):catch しなくてもコンパイルできる// 発生するとプログラムが終了するため、事前にバリデーションするのが基本int result = 10 / 0; // ArithmeticException(ランタイムエラー)実務での判断基準:
- 呼び出し元が適切に処理できる状況(ファイルが存在しない、など)→ 検査例外
- プログラムのバグ(null渡し、不正な引数など)→
IllegalArgumentExceptionなどの非検査例外
try-catch-finally
Section titled “try-catch-finally”public int divide(int a, int b) { try { return a / b; } catch (ArithmeticException e) { System.err.println("ゼロ除算エラー: " + e.getMessage()); return 0; } finally { System.out.println("finallyは必ず実行される"); // try/catchのどちらでもここを通る // リソースの解放処理などを書く }}複数の例外を捕捉
Section titled “複数の例外を捕捉”try { String s = null; int n = Integer.parseInt(s); // NullPointerException} catch (NullPointerException e) { System.err.println("nullです: " + e.getMessage());} catch (NumberFormatException e) { System.err.println("数値に変換できません: " + e.getMessage());} catch (RuntimeException e) { // 上で捕捉されなかった RuntimeException をまとめて捕捉 System.err.println("予期しないエラー: " + e.getMessage());}
// マルチキャッチ(同じ処理をする場合は | で並べられる)try { // ...} catch (NullPointerException | NumberFormatException e) { System.err.println("入力エラー: " + e.getMessage());}try-with-resources
Section titled “try-with-resources”Closeable(AutoCloseable)を実装したリソースを自動的にクローズする構文。finally でのクローズ忘れを防ぐ。
// 旧来の書き方(finally で close)FileReader reader = null;try { reader = new FileReader("file.txt"); // 処理} catch (IOException e) { // エラー処理} finally { if (reader != null) { try { reader.close(); } catch (IOException ignored) {} }}
// try-with-resources(Java 7以降):自動クローズtry (FileReader reader = new FileReader("file.txt")) { // 処理 // tryブロックを抜けると自動的に reader.close() が呼ばれる} catch (IOException e) { System.err.println(e.getMessage());}
// 複数のリソースも扱える(逆順でクローズされる)try (var conn = DriverManager.getConnection(url); var stmt = conn.createStatement()) { // ...}独自例外クラス
Section titled “独自例外クラス”業務ロジック固有のエラーは独自例外として定義する。
// 独自例外の定義public class BookNotFoundException extends RuntimeException { private final long bookId;
public BookNotFoundException(long bookId) { super("書籍が見つかりません: ID=" + bookId); this.bookId = bookId; }
public long getBookId() { return bookId; }}
// 使用例public Book findById(long id) { return books.stream() .filter(b -> b.id() == id) .findFirst() .orElseThrow(() -> new BookNotFoundException(id));}
// 呼び出し元try { Book book = findById(999L);} catch (BookNotFoundException e) { System.err.println(e.getMessage()); // "書籍が見つかりません: ID=999" System.err.println("BookId: " + e.getBookId());}Optional の実践パターン
Section titled “Optional の実践パターン”null の代わりに Optional を返す
Section titled “null の代わりに Optional を返す”// nullを返す設計(非推奨)public Book findByTitle(String title) { for (Book book : books) { if (book.title().equals(title)) return book; } return null; // nullを返すと呼び出し元でnullチェックが必要になる}
// Optional を返す設計(推奨)public Optional<Book> findByTitle(String title) { return books.stream() .filter(b -> b.title().equals(title)) .findFirst();}
// 呼び出し側findByTitle("Java入門") .map(Book::price) // Bookがある場合のみpriceを取得 .filter(price -> price < 3000) // 価格が3000未満なら .ifPresent(price -> System.out.println("おすすめ価格: " + price));
findByTitle("Spring実践") .ifPresentOrElse( book -> System.out.println("見つかった: " + book.title()), () -> System.out.println("見つかりません") );
// 存在しない場合のデフォルトint price = findByTitle("存在しない本") .map(Book::price) .orElse(0);ifPresent() は「値があるときだけ処理したい」場合に向く。値がない場合の処理も同じ場所に書きたいなら ifPresentOrElse() を使う。
Optional のアンチパターン
Section titled “Optional のアンチパターン”// アンチパターン1:get() を直接使う(空なら NoSuchElementException)Optional<Book> opt = findByTitle("Java");Book book = opt.get(); // 危険!空なら例外
// 正しい書き方Book book = opt.orElseThrow(() -> new BookNotFoundException("Java"));
// アンチパターン2:Optional をフィールドに持つ(シリアライズできない)public class User { private Optional<String> email; // NG}
// 正しい書き方:フィールドは素直にnullableで持ち、メソッドでOptionalを返すpublic class User { private String email; // nullable
public Optional<String> getEmail() { return Optional.ofNullable(email); }}例外処理の設計パターン
Section titled “例外処理の設計パターン”ガード節(早期リターン)
Section titled “ガード節(早期リターン)”// ネストが深くなる書き方public String processOrder(Order order) { if (order != null) { if (order.isValid()) { if (order.hasStock()) { return "処理完了"; } else { return "在庫なし"; } } else { return "注文が無効"; } } else { return "注文がnull"; }}
// ガード節(推奨):条件を反転して早期リターンpublic String processOrder(Order order) { if (order == null) return "注文がnull"; if (!order.isValid()) return "注文が無効"; if (!order.hasStock()) return "在庫なし"; return "処理完了";}例外の変換(ラッピング)
Section titled “例外の変換(ラッピング)”低レベルの例外を、業務的に意味のある例外に変換する。
public Book loadFromFile(String path) { try { String json = Files.readString(Path.of(path)); return parseBook(json); } catch (IOException e) { // IOExceptionをアプリ固有の例外に変換 throw new BookLoadException("書籍ファイルの読み込みに失敗: " + path, e); // cause(元の例外)も一緒に渡すことで、スタックトレースが失われない }}| 項目 | ポイント |
|---|---|
| 検査例外 | throws 宣言か catch が必要。呼び出し元が対処できる場合に使う |
| 非検査例外 | プログラムのバグには IllegalArgumentException 等を使う |
| try-with-resources | Closeableなリソースは自動クローズ構文で管理する |
| 独自例外 | 業務固有のエラーは意味のある名前の例外クラスに |
| Optional | nullの代わりに返し、呼び出し元にnullチェックを強制する |
| ガード節 | 早期リターンで条件のネストを浅くする |