コンテンツにスキップ

9-4. 例外処理・null安全・Optionalの実践

  • 検査例外と非検査例外の違いと使い分けを理解する
  • try-catch-finally と try-with-resources の使い方を習得する
  • 独自例外クラスの定義と例外の変換パターンを学ぶ
  • Optional を実務で正しく使うパターンとアンチパターンを知る
  • ガード節(早期リターン)による読みやすいコードの書き方を学ぶ

第4章で例外処理の概念を学んだ。ここではJavaでの具体的な実装パターンを習得する。

Throwable
├── Error(回復不可能な深刻なエラー)
│ ├── OutOfMemoryError
│ ├── StackOverflowError
│ └── ...
└── Exception(プログラムで処理できるエラー)
├── RuntimeException(非検査例外:throws宣言不要)
│ ├── NullPointerException
│ ├── IllegalArgumentException
│ ├── IllegalStateException
│ ├── ArrayIndexOutOfBoundsException
│ └── ...
└── 検査例外(throws宣言またはcatch必須)
├── IOException
├── SQLException
└── ...
// 検査例外(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 などの非検査例外
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のどちらでもここを通る
// リソースの解放処理などを書く
}
}
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());
}

CloseableAutoCloseable)を実装したリソースを自動的にクローズする構文。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()) {
// ...
}

業務ロジック固有のエラーは独自例外として定義する。

// 独自例外の定義
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());
}
// 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() を使う。

// アンチパターン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);
}
}
// ネストが深くなる書き方
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 "処理完了";
}

低レベルの例外を、業務的に意味のある例外に変換する。

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-resourcesCloseableなリソースは自動クローズ構文で管理する
独自例外業務固有のエラーは意味のある名前の例外クラスに
Optionalnullの代わりに返し、呼び出し元にnullチェックを強制する
ガード節早期リターンで条件のネストを浅くする