こんにちは。アグリゲーション開発を担当しています中川です。
マネーフォワードのアグリゲーション部分は、通称「アグリ村」と呼ばれるCTO直轄部隊によってJava言語で開発しています。 注:ムラ社会というわけではありません(笑)
そこで今回は、Javaに関連した投稿をしようと思います。
はじめに
Javaをはじめとする言語には、例外処理という便利な仕組みが提供されています。例外処理によって、エラーを簡単にかつ適切に処理することが可能です。
しかし、使用方法を誤ってしまうと、例外処理のメリットが発揮できなかったり、発生・検出したエラーをもみ消してしまったり。。。
本稿では、そんな失敗をしないためのベスト・プラクティスを紹介します。
例外を無視しない/破棄しない
例外は処理実行時に発生した問題を示してくれます。そのため、例外を無視や破棄をしてしまうと、不正なプログラムやデータなどを検出する機会を失ってしまうともに、不正な状態のままシステムが動き続けてしまい、二次障害にもつながる可能性があります。
例外をキャッチしたにも関わらず、無視・破棄してしまうは絶対にダメ。 もしそんなコードを先輩に見つけられたら、正座で説教は免れられないでしょう。
イケてないコードの例:
void doSomething() { try { foo(); } catch (IOException e) { e.printStackTrace(); } }
このコードはIOException
をキャッチし、e.printStackTrace();
を行った後、何事も無かったかのように続行していますよね。doSomething()
の呼び元からは、処理が正しく行われたかどうを分かる術が有りません。
この例から学ぶポイント
e.printStackTrace();
と書いただけでは例外に対応した事にはなりません。監視しているプログラマーが問題に気づくことが出来るかもしれませんが、プログラム事態は何事も無かったかのように進んでしまいます。
では、代わりにどうすればよいのでしょうか?
1.doSomething()
が I/O 関係の例外をスローすることがおかしくない場合は、IOException
をそのままスローするとよいでしょう。
void doSomething() throws IOException { foo(); }
2.doSomething()
がIOException
をスローするがおかしい場合は、別の例外クラスでラッピングしましょう。
void doSomething() throws MySpecificException { try { foo(); } catch (IOException e) { throw new MySpecificException("failed my specific task", e); } }
3.例外がチェック例外(Exception)の場合は何かしらの例外処理が必要ですが、発生することが想像できない場合は非チェック例外(RuntimeException・Error等)にしてスローするのも1つの手段です。 深刻な情報が破棄されるのに比べば、この書き方もアリです。但し、乱用注意です。
void doSomething() { try { foo(); } catch (IOException e) { // This cannot happen throw new AssertionError(e); } }
【補足】
元の例外は cause
引数に渡しておくと、デバッグ時に役立ちます。
cause
引数に渡した例外は、スタックトレースの印字時などにCauseとして印字されるため、原因調査に活用することができます。
例外は必要最低限の範囲でキャッチしましょう
下記は悪いコードの例です。
void doSomething() { try { readFromConfig(); } catch (Exception e) { // file could be missing handleMissingFile(); } }
catch (Exception e) {
の記述は大抵の場合必要ありません。キャッチする範囲が広すぎると余計な例外も拾われてしまいます。例えばNullPointerException
が発生してもキャッチされてしまいます。当たり前ですが、NullPointerException
をキャッチしてファイル不足として対処するのは適切ではありません。
通常なエラー処理でException
、RuntimeException
、Throwable
の範囲でキャッチする必要はありません。必要以上に大きな範囲でキャッチするのは、余計な例外を拾って不正な対処を行ったり、例外の破棄につながります。
throws Exceptionではなくサブクラスを書きましょう
main
メソッドなど、プログラムの外枠ではthrows Exception
と書くのが正しい場合もありますが、内側のメソッドでthrows Exception
と書かれてしまうと、呼び元はcatch (Exception e) {
と書かざる得なくなってしまいます。(前述の「例外は必要最低限の範囲でキャッチしましょう」より)
それにthrows Exception
だと、呼び元としては、例外が発生したことは認識できますが、どのような例外が発生したのかが不明確です。
Exception
ではなく、具体的なサブクラスをスローするように書きましょう。
void doSomething() throws IOException { foo(); try { bar(); } catch (NoSuchMethodException e) { handleMissingMethod(); } }
更にこの例では、具体的なIOException
を書いたことによって、仮にNoSuchMethodException
の対処が漏れたとしてもコンパイルエラーとなり気づくことができます。スローがException
だとコンパイルは通り実行時にNoSuchMethodException
が発生した場合に上へ流れてしまいます。
例外の後始末を忘れずに
メソッド内で例外が発生し処理が続行できない場合でも、戻る前の後始末は大切です。後始末を行いわないメソッドは、呼び側のコードで余計な考慮が必要となるため全体的な設計が複雑になりがちです。
常に後始末に心掛けてメソッドを実装すると、こういった余計な作業も避けられます。
メソッド内で例外が起きた場合、一般的には呼ばれる前と同じ状態で戻るのが理想です。
Javaでは後始末の手助けするための構文も用意されています。
- 常に行わないといけない後始末は
finally {}
を用いえるとよいでしょう。 - JDK7からは、
AutoCloseable
を実装しているクラスに置いて以下のような構文でリソースの解放を保証することが可能となりました。
// os は以下のブロックを抜ける時に必ず閉じられる try (OutputStream os = Files.newOutputStream(Paths.get("foo.txt"))) { // ... }
Java の標準的な例外クラスを使いましょう
以下は、ほんの一部の代表的な標準例外クラスです:
- NullPointerException
- IllegalArgumentException
- IllegalStateException
- AssertionError
エラーの意味合いが標準クラスと一致する場合はそのまま標準クラスを使いましょう。 逆に、一致しない場合は、無理に使ってはいけません。騙しな例外クラスに迷いながら、デバッグに無駄な時間を費やす同僚が可哀そうです。
NullPointerException
の場合は JDK7から以下のチェックを短く書くObjects
クラスが提供されていますね。
if (argument == null) { throw new NullPointerException(); }
JDK7:
Objects.requireNotNull(argument);
まとめ
例外処理を適切に使いこなせば、簡単にかつデバッグしやすい信頼性の高いコードを組むことができます。 その一方、不適切な使い方をすると、色々なところに犠牲者を出してしまいます。
本稿では、私の感想的な要素も取り込んだベスト・プラクティスでしたが、是非とも興味を持ってコミュニティーボードやブログを参考にしてみてください。
リファレンス
http://www.onjava.com/pub/a/onjava/2003/11/19/exceptions.html?page=2 http://howtodoinjava.com/2013/04/04/java-exception-handling-best-practices/ http://www.journaldev.com/1696/java-exception-handling-tutorial-with-examples-and-best-practices http://programmers.stackexchange.com/questions/231057/exceptions-why-throw-early-why-catch-late http://docs.oracle.com/javase/tutorial/essential/exceptions/