社内se × プログラマ × ビッグデータ

プログラミングなどITに興味があります。

Java 例外処理における 11 個の誤り

Java の例外処理における tips を見つけたんですが、自分にはとても分かりやすかったので、感想と共に書き残しておこうと思います。

11 Mistakes Java Developers make when Using Exceptions
https://www.linkedin.com/pulse/11-mistakes-java-developers-make-when-using-rafael-chinelato-del-nero

1. Exception クラスしか使わないこと

どんな例外をキャッチする場合も、Exception クラスで定義してしまっているということですね。
まぁ、例外が発生したらその種類に関わらず、アプリケーションを止めてしまうようなケースであれば、楽と言えば楽と思いますが。
別の見方をすれば、どんな例外が発生するのか開発者が想定出来ていないということなのかもしれません。

基本的には Exception クラスを継承した固有の例外クラスを定義してあげて、スロー・キャッチしてあげるようにするのが良いのかもしれません。
Exception クラスでキャッチするということは、本当に想定外の例外が発生した場合になるのかもしれません。

2. 固有の例外クラスを作り過ぎてしまうこと

確かに例外が発生する箇所ごとに、例外クラスを作っていたら例外クラスだらけになってしまいますね。
自分は基本的にキャッチ例外と非キャッチ例外の2つしか作らないのですが、この記事に書かれているように、ビジネス要件のレベルで例外を作成するのも良いなと思いました。
その例外の名前を見るだけで、大体の例外内容やビジネスインパクトなどが分かるくらいが良いですね。

例)
ApplicationFailException よりも ApiNotAvailableException の方が何が問題なのか分かります。

3. すべての例外キャッチ時にログを残すこと

ログを残すことは良いことに思えますが、例外をキャッチする度にログを残していると、最終的に同じエラー内容が何度も残されてしまうかもしれません。
ログを残すにしても、スロー元ですでにロギングされている内容でないか?また、スロー先でまとめてロギング出来ないかを考慮して残すようにしたいですね。

4. キャッチ例外(Exceptionクラスを継承)と、非キャッチ例外(RuntimeExceptionを継承)の違いを知らないこと

自分が初めて固有の例外クラスを作成したときは、とにかく非キャッチ例外を使うようにしていました。
理由は単に楽だからです。そこにそれぞれの例外の違いなどは特に意識していません。
非キャッチ例外でスローしておけば、呼び出し元でわざわざキャッチ処理を書く必要がありませんから。

それぞれの役割を知るようになってからは、使い分けるように(一応)意識するようになりました。
この使い方については、さまざまな意見があるようですが自分の中では以下の使い分けで落ち着いています。

リカバリ可能な例外であれば、キャッチ例外。
リカバリ不可能(あるいは不要)な例外であれば、非キャッチ例外。

キャッチ例外でスローしておけば、呼び出し元で改めて try - catch しないといけないので。
意図的にその例外をハンドリングしていることを明示していることにもなります。

5. 無音の例外があること

Silencing を無音と直訳してしまってますが、つまりは例外が発生しないところでキャッチしようとしていることですね。
以下は極端な例ですが、全く意味のない処理になります。

try {
  // Nothing to do
} catch (Exception e) {
  // Nothing to do
}

こんなことするわけない!と思いがちですが、「どんな例外が発生するか分からないから、とりあえず try - catch しとこう」みたいなコーディングは、同じことをしているのかもしれません。
自分も覚えがあります。

6. "throw early, catch late" の原則に従っていないこと

この原則は有名らしいのですが、聞いたことがなかったです。
自分なりに調べたところ、
1) throw early -> 問題の原因を見つけやすくするために、出来るだけ早く例外を投げること
2) catch late -> その例外の対処は、高い階層であればあるほど対処しやすい(どこからリトライさせるのか)

というように理解しています。

7. 例外に対して、明確なメッセージを使っていない

単に「エラーが発生しました」「処理に失敗しました」では何が悪かったのか分からない。
より具体的に「◯◯のデータベースの××テーブルが見つかりません」のように書いてあった方が原因がすぐに分かる。

8. 例外を処理した後に、クローズ系の後処理をしていない

せっかく try - catch - finally があるのだから、finally においてデータベースコネクションのクローズなんかもしておくと、パフォーマンス的には良いかもしれません。

try(Dao mysqlDao = new Dao()) {}

try-with-resources で書いておければ、毎回 close 書かなくて済みます。

9. javadoc に例外についての文書が残されていない

個人的に一番気をつけたいと思った項目。
何故、その例外を作ったのか?って全く書いていないので、意識して書くようにしたいです。

10. Stacktrace(トレース)を失うこと

英語の訳は Never lose the Stacktrace -> Stacktrace を決して失わないこと のように思いますが、文書からするとトレースを失うことが問題のように思えます。
個人的にはよく「例外の情報源を握りつぶす」と呼んでいます。

11. 固有例外が階層で整理されていないこと

Java の例外クラスが階層で定義されているのと同じように、固有で作成する例外についても階層で整理するようにしなさいってことですね。
自分は2つ以上の固有例外クラスを作ったことがないので、あまり実感はないのですが、例外の発生箇所(階層)によって例外を定義するのであれば、やはり階層を意識した作りにした方が管理しやすいだろうなと思います。

まとめ
  • 例外処理は面倒だけど、意味のある例外処理を書けるということは、そのプログラムを理解出来ているということだと思う
  • 意味のある例外処理を書くには、何故その例外が発生するのかを理解する必要がある
  • 理由をコメント javadoc で残すようにしよう