Java の例外について情報をまとめる
例外の種類(継承関係)
Java の例外クラス Throwable, Error, Exception, RuntimeException の階層を整理
Exception
を継承した例外は検査例外となりthrows
や try-catch
して例外処理しないとコンパイルエラーとなる
RuntimeException
を継承した例外は非検査例外となり例外処理しなくてもコンパイルエラーにならない
Throwable
, Error
を明示的に使うケースあまりない(使う時はどんな例外も絶対に Java の外に出したくない時など...)
クラス | 概要 | 例 |
---|---|---|
Throwable | 例外の基底クラス throws/try-cathc必要 |
- |
Error | 致命的エラー throws/try-cathc不要 |
StackOverflowError OutOfMemoryError |
Exception | 検査例外 throws/try-cathc必要(ないとコンパイルエラー) |
IOException SQLException |
RuntimeException | 非検査例外 throws/try-cathc不要 検査例外をラップしたExceptionを作ることがある |
NullPointerException NumberFormatException |
Exception 検査例外
呼び出し元に何らかの例外処理を強制する例外
throws
や try-catch
しないとコンパイルエラーになる
例外を処理しない時は例外情報を失わないために throws
で呼び出し元に例外を投げる
- メソッド宣言 throws で呼び出し元に例外を投げる
public void method() throws IOException {
List<String> lines = Files.readAllLines(Paths.get("xxx"));
...
}
- 処理内で try-catch して例外処理
try {
List<String> lines = Files.readAllLines(Paths.get("xxx"));
...
} catch (IOException e) {
// 例外処理
...
}
RuntimeException 非検査例外
呼び出し元に例外処理を強制しない例外
例外処理をしなくてもコンパイルエラーにならない(実行時例外になる)
検査例外をラップして非検査例外にすることがある
そうすることで呼び出し元のメソッド定義を変えずにコンパイルエラーにならずに処理できる
- 検査例外を非検査例外でラップ
try {
List<String> lines = Files.readAllLines(Paths.get("xxx"));
...
} catch (IOException e) {
throw new RuntimeException(e);
}
例外処理の文法
Java の例外処理の文法を整理
文法としては「メソッド宣言 throws
」、「処理内 try-catch
」の2つある
Exception
検査例外 が発生するときは記載がないとコンパイルエラーになる
RuntimeException
非検査例外のときは記載不要だが明示的に記載し JavaDoc にコメントを残すことで例外を他の開発者に伝えることができる
メソッド宣言 throws
呼び出し元のクラスで例外処理を行ってもらうときの記載方法
発生する例外に対して throws
に記載していく
public void method() throws IOException, ParseException { // 「,」区切りで複数記載OK
List<String> lines = Files.readAllLines(Paths.get("xxx"));
Object text = new DateFormatter().stringToValue("test");
...
}
処理内 try-catch-finally
処理内で例外処理を行うときの記載方法
発生する例外に対して catch
に記載していく
catch
した後の例外処理で throw
することもできる
その場合、メソッド宣言 throws
と合わせて記載する
例外発生に関係なく必ず実行したい処理があれば finally
の中に記載する
try {
List<String> lines = Files.readAllLines(Paths.get("xxx"));
Object text = new DateFormatter().stringToValue("test");
Thread.sleep(1);
...
} catch (IOException | ParseException e) { // 「|」で複数記載OK
// 例外処理
...
} catch (InterruptedException e) { // 「catch」は複数記載OK
// 例外処理
...
// throw することも可能(その場合、メソッド宣言 throws も必要)
throw e;
} finally {
// 例外発生に関係なく必ず実行する処理
...
}
- リソースクローズ try-with-resources
ファイル、DBなどの外部リソースにアクセスするときに接続を断つために close
処理を実行する必要がある
close
しないと接続が開いた状態が続くことがあり予期せぬバグにつながることがある
Java では try-with-resources
を使うことで対応できる(AutoCloseable
の実装クラスのみ)
// try() の中で変数宣言したものが close 対象、「;」で複数行記載OK
try (BufferedReader br = Files.newBufferedReader(Paths.get("xxx"))) {
...
} catch (IOException e) {
...
}
- 例外ラップ
例外を別の例外にして throw
するときは元の例外をラップして例外情報を失わないようにする
例外をラップしないで別の例外を生成すると元の例外情報が失われてしまいバグの原因が分からなくなってしまう
例外を別の例外にラップするするケースとしては以下2つが考えられる
- アプリ(機能)独自の例外にして例外処理を統一する
- 検査例外を非検査例外にして呼び出し元に影響(コンパイルエラー)が出ないようにする
try {
List<String> lines = Files.readAllLines(Paths.get("xxx"));
...
} catch (IOException e) {
// e(例外) を別の例外を生成するときに引数として渡す
throw new RuntimeException(e);
// throw new RuntimeException("error message", e);
}
例外処理でダメなこと
例外処理において例外を隠蔽(握り潰す)したり変えてしまうと原因の特定ができなくなってしまう
また広範囲(Excetion
など)な例外を throws
や catch
すると想定外の例外でも処理してしまいバグに気づかなくなってしまう
例外処理でやらないほうがよいことを記載
例外を隠蔽(握り潰す)
例外を catch
した後に処理しないで終わると隠蔽したことになる
例外処理をしないクラスでは catch
しないで呼び出し元に throw
する(メソッド宣言throws
のみでOK)
例外処理をするときでも catch
して処理を行った後に呼び出し元に throw
する(メソッド宣throws
+ catch
した処理内で throw
が必要)
意図的に throw
しない時は log出力など例外情報を記録した方が良い
何も情報を残さないと想定外の例外が放置されてしまい別の問題を引き起こす可能性がある
- 隠蔽(握り潰す)
try {
List<String> lines = Files.readAllLines(Paths.get("xxx"));
...
} catch (IOException e) {
// 処理しないで握り潰す
// 意図的にやるときでも log 出力してほしい、明らかに不要なときでもコメントに意図を記載
}
- 呼び出し元に throw(メソッド宣言 throws のみ)
public void method() throws IOException {
List<String> lines = Files.readAllLines(Paths.get("xxx"));
...
}
- 呼び出し元に throw (catch して処理して throw)
public void method() throws IOException {
try {
List<String> lines = Files.readAllLines(Paths.get("xxx"));
...
} catch (IOException e) {
...
throw e;
// 別の Exception にラップ(設定)するでもOK
// throw new XxxException(e);
// throw new XxxException("messeage", e);
}
}
例外を消失
catch
した例外をラップ(設定)しない 'Exception' を生成して throw
すると例外情報を失うことになる
例外処理で新しく Exception を生成するときは基本的には catch
した Exception
をラップ(設定)し例外情報を失わないようにする
意図的にラップ(設定)しない時は log出力など例外情報を記録した方が良い
何も情報を残さないと想定外の例外が放置されてしまい別の問題を引き起こす可能性がある
- 消失
try {
List<String> lines = Files.readAllLines(Paths.get("xxx"));
...
} catch (IOException e) {
// e をラップ(設定)しない Exception 生成で元の例外を消失
// 意図的にやるときでも log 出力してほしい、明らかに不要なときでもコメントに意図を記載
throw new XxxException("message");
}
- 例外をラップ(設定)
try {
List<String> lines = Files.readAllLines(Paths.get("xxx"));
...
} catch (IOException e) {
// e をラップ(設定)すればOK
throw new XxxException("message", e);
}
広範囲な例外指定
throws
や catch
に Exception
などの広範囲な例外クラスを使うと想定外の例外に対しても処理してしまう
そのため想定した例外以外が発生したときに例外が正しく処理されないことがある
とくに意図的に隠蔽などをおこなった箇所でやっていると例外情報が失われてしまう
- 余計な例外処理
// Exception ではなく IOException など発生する例外のみにする
public void method() throws Exception {
try {
List<String> lines = Files.readAllLines(Paths.get("xxx"));
...
} catch (Exception e) {
// Exception ではなく IOException など発生する例外のみにする
...
throw e;
}
}
例外処理でスタックトレース出力のみ
catch
した例外処理の中で e.printStackTrace()
のみしか行わないとアプリ停止や設定によって例外情報が失われてしまう
結果として隠蔽(握り潰す)と同じことになる
教科書などのサンプルだと多いが実際のアプリではスタックトレース出力ではなくログ出力していることが多い
try {
List<String> lines = Files.readAllLines(Paths.get("xxx"));
...
} catch (IOException e) {
// これのみだと隠蔽と同じことになる
// throw して呼び出し元で処理するか log 出力して例外情報を残してほしい
e.printStackTrace;
}