はじめに
Javaを学び始めると、例外処理は早い段階でつまずきやすいテーマのひとつです。
「とりあえず try-catch で囲めばいい」「throws Exception と書けばコンパイルが通る」という理解のまま進んでしまうと、バグの原因に気づきにくいコードになってしまうことがあります。
この記事では、例外処理を基礎から理解したい方に向けて、なぜそう書いてはいけないのかを、検査例外・非検査例外の違い、throws の正しい使いどころ、やってはいけないアンチパターンまで、順を追って解説します。
例外の全体像
まず、Javaの例外クラスの階層を把握しましょう。
Throwable
├── Error ← JVMレベルの深刻なエラー(基本的に対処不可)
│ ├── OutOfMemoryError
│ └── StackOverflowError
│
└── Exception ← プログラムで対処できる例外
├── IOException ← 検査例外(Checked Exception)
├── SQLException ← 検査例外
├── ClassNotFoundException ← 検査例外
│
└── RuntimeException ← 非検査例外(Unchecked Exception)
├── NullPointerException
├── IllegalArgumentException
├── IndexOutOfBoundsException
└── ClassCastException
Exception のサブクラスは大きく2種類に分かれます。
-
検査例外(Checked Exception):
RuntimeException以外のException -
非検査例外(Unchecked Exception):
RuntimeExceptionとそのサブクラス
検査例外(Checked Exception)とは?
特徴
コンパイラが「必ず処理しなさい」と強制する例外です。
try-catch でキャッチするか、throws で呼び出し元に委ねないと、コンパイルエラーになります。
// IOException は検査例外なので、処理しないとコンパイルエラー
public void readFile(String path) {
BufferedReader reader = new BufferedReader(new FileReader(path));
// コンパイルエラー! IOException を処理していない
}
対処方法は2つあります。
// ① try-catch でキャッチする
public void readFile(String path) {
try (BufferedReader reader = new BufferedReader(new FileReader(path))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
System.err.println("ファイルの読み込みに失敗しました: " + e.getMessage());
}
}
// ② throws で呼び出し元に委ねる
public void readFile(String path) throws IOException {
try (BufferedReader reader = new BufferedReader(new FileReader(path))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
}
}
代表的な検査例外
| 例外クラス | 発生する場面 |
|---|---|
IOException |
ファイル・ネットワーク操作 |
FileNotFoundException |
ファイルが見つからない |
SQLException |
DB操作 |
ClassNotFoundException |
クラスが見つからない |
ParseException |
日付などのパース失敗 |
なぜ検査例外が存在するのか?
「このメソッドを呼び出す人は、必ずエラーのことを考えてください」というJavaの設計思想です。ファイルやDBのような外部リソースへのアクセスは失敗しうるため、呼び出し元に対処を強制しています。
非検査例外(Unchecked Exception)とは?
特徴
処理しなくてもコンパイルが通る例外です。RuntimeException のサブクラスが該当します。
// NullPointerException は非検査例外なので、try-catch がなくてもコンパイルが通る
public void printLength(String str) {
System.out.println(str.length()); // strがnullだとNullPointerExceptionが発生
}
代表的な非検査例外
| 例外クラス | 発生する場面 |
|---|---|
NullPointerException |
nullオブジェクトのメソッド呼び出し |
IllegalArgumentException |
不正な引数が渡された |
IllegalStateException |
オブジェクトの状態が不正 |
IndexOutOfBoundsException |
配列・Listの範囲外アクセス |
ClassCastException |
不正なキャスト |
ArithmeticException |
ゼロ除算など |
なぜ非検査例外が存在するのか?
プログラムのバグが原因の例外は、毎回 try-catch で囲むのではなく、そもそもバグを直すべきという考え方があります。NullPointerException はnullチェックを適切にすれば防げます。全部 try-catch で囲むとコードが冗長になるため、非検査例外はコンパイラの強制なしに任せています。
try-catch-finally の使い方
try {
// 例外が発生しうる処理
int result = 10 / divisor;
} catch (ArithmeticException e) {
// 例外が発生したときの処理
System.err.println("計算エラー: " + e.getMessage());
} catch (Exception e) {
// その他の例外(広いものは後に書く)
System.err.println("予期しないエラー: " + e.getMessage());
} finally {
// 例外の有無にかかわらず必ず実行される
System.out.println("処理終了");
}
try-with-resources(Java 7以降)
Closeable を実装したリソース(ファイル、DB接続など)は、try-with-resources を使うと自動でクローズされます。
// try-with-resources を使う(推奨)
try (BufferedReader reader = new BufferedReader(new FileReader("file.txt"))) {
String line = reader.readLine();
System.out.println(line);
} catch (IOException e) {
System.err.println(e.getMessage());
}
// reader.close() は自動で呼ばれる
// 昔の書き方(finally で手動クローズ)
BufferedReader reader = null;
try {
reader = new BufferedReader(new FileReader("file.txt"));
System.out.println(reader.readLine());
} catch (IOException e) {
System.err.println(e.getMessage());
} finally {
if (reader != null) {
try { reader.close(); } catch (IOException e) { /* ignore */ }
}
}
throws の使いどころ
throws はメソッドシグネチャに書き、「このメソッドはこの例外を投げることがある」と呼び出し元に伝えるためのものです。
// throws を使う例
public User findUser(int id) throws UserNotFoundException {
User user = userRepository.findById(id);
if (user == null) {
throw new UserNotFoundException("ユーザーが見つかりません: id=" + id);
}
return user;
}
throws を使う判断基準
| 状況 | 対応 |
|---|---|
| 呼び出し元がエラーに対処できる |
throws で委ねる |
| このメソッド内で完結して対処できる |
try-catch でキャッチする |
| プログラムのバグが原因 |
throws も try-catch も不要(バグを直す) |
自分で例外を投げる(throw)
独自のバリデーションなどで、自分で例外を投げることができます。
public void setAge(int age) {
if (age < 0 || age > 150) {
throw new IllegalArgumentException("年齢が不正です: " + age);
}
this.age = age;
}
独自例外クラスを作る
// 検査例外として作る場合
public class UserNotFoundException extends Exception {
public UserNotFoundException(String message) {
super(message);
}
}
// 非検査例外として作る場合
public class InvalidOrderException extends RuntimeException {
public InvalidOrderException(String message) {
super(message);
}
}
やってはいけないアンチパターン
① 例外を握り潰す
// ❌ 最悪のパターン:例外が発生しても何もしない
try {
riskyOperation();
} catch (Exception e) {
// 何も書かない(もしくはコメントアウト)
}
// ✅ 最低でもログを出す
try {
riskyOperation();
} catch (Exception e) {
logger.error("処理に失敗しました", e);
throw e; // 必要であれば再スロー
}
② throws Exception の乱用
// ❌ 何でもかんでも throws Exception
public void doSomething() throws Exception {
// ...
}
// ✅ 具体的な例外を指定する
public void doSomething() throws IOException, SQLException {
// ...
}
throws Exception は「どんな例外が起きるかわからない」というメッセージになり、呼び出し元が適切に対処できなくなります。
③ catch (Exception e) で何でもキャッチする
// ❌ すべての例外を一括でキャッチ(バグが隠れる)
try {
String result = processData(null);
} catch (Exception e) {
System.out.println("エラー");
}
// ✅ 想定する例外を具体的にキャッチ
try {
String result = processData(null);
} catch (NullPointerException e) {
System.out.println("nullが渡されました");
} catch (IllegalArgumentException e) {
System.out.println("不正な引数です: " + e.getMessage());
}
まとめ
- 検査例外:コンパイラが処理を強制。外部リソース操作など「失敗しうる」処理に使う
- 非検査例外:処理は任意。プログラムのバグが原因のことが多い
-
throws:呼び出し元が対処すべき例外を委ねるときに使う -
アンチパターン:例外の握り潰し・
throws Exceptionの乱用・catch (Exception e)の乱用は避ける