こんにちは!
田村です。
これは、TECHSCORE Advent Calendar 2016 の23日目の記事です。
今日はあまり経験のない中、例外処理を書いてみたつもりが、例外発生時に
「調査しても何もわからない、例外処理にもなっていないヤバイコードを書いていた…!」
という怖~い話をします。
この記事を読んで、先輩エンジニアの方には新人ってこんなことで躓いているんだなぁと知っていただき、新人エンジニアの方には、私のしくじりから1つでも多く学んでいただければと思います。
学んだこと
今回記事にしたことをきっかけに学んだことは以下です。
あとで順を追ってみていきます。
- 例外を握り潰してはいけない
- 例外発生時にリソースリークが起こらないように注意する
- 例外処理は catch して throw すれば良いとは限らない
自己紹介
先に軽く自己紹介をします。
情報系大学を卒業してから、4月に入社し、6月から開発職として働き始めました。
Javaの基本的な文法はわかっていたのですが、入社までにJavaで特に何かを作ったという経験はなく、仕事では既存機能のコードを見て知らないクラスを見つけては、Javaのドキュメントとにらめっこしながら、開発する日々を送っています。
「例外処理」という言葉をテキストで出てきたなぁぐらいでしか認識しておらず、例外処理を実装するのは初めてでした。
本題
あるとき、以下のようなコードを作りました(学んだことを伝えるためのサンプルコードです)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
public void outputNameList() { OutputStream outputStream = ...; // 初期化していることにします。 OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream); List<String> nameList = new ArrayList<String>(); nameList.add("太郎"); nameList.add("次郎"); nameList.add("花子"); try { writeNameList(nameList, outputStreamWriter); outputStreamWriter.close(); outputStream.close(); } catch (Exception e) { // ここは既存のコードで正しい例外処理が書かれてました } } private void writeNameList(List<String> nameList, OutputStreamWriter writer) { for (String name : nameList) { try { writer.write(name); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } |
22-27行目は、Eclipseでtry/catchを自動生成すると、こんな感じで書かれることがありますよね。
これで赤い波線でEclipseに怒られることもなくなったし、さて次の機能を実装しよう!
…と意気込んでいたら、先輩から「待った」をいただきました。
「待った」の理由は例外を握り潰しているからです。
1.例外を握りつぶしてはいけない
ネットで調べてみると、色んなところでサンプルコードの22-27行目のような記述をよく見かけます。
Javaの入門書の例外処理に関するところ見ても大した説明もなく書いてあります。
これは、例外が発生したときに、
「何行目のなんとかメソッドでなんらかの例外が発生したよー」
と標準エラー出力するようになっています。
当然、自分で勉強用に書いているプログラムであれば、問題ないと思います。
しかし、私の開発しているアプリケーションでは、例外を検知するために通知メールを送信するなどの例外処理を書いたりします。
printStackTrace()メソッドでログを書き出すだけでは誰も検知できないので、そのまま呼び出し元に戻って、closeしてプログラムが終了します。
ログに書き出すだけということは、例外が発生しても誰も検知できず、対処もできないということです。
例外が発生したことをなかったことにしてハンドリングしないことを例外を握りつぶすといいます。
実際のプログラムにサンプルコードのような記述があれば、例外を握りつぶすことで、エラーが残ったまま、さまざまな処理が進んでいくでしょう。
何かの処理中に残ってしまったエラーが規模の大きい深刻なエラーへ発展しかねません。
そのようなことが起きないように、例外を握りつぶすような記述が意図的でないのであれば書かないように気をつけようと思いました。
2.例外発生時にリソースリークが起こらないように注意する
例外処理を握りつぶしていたので、下記のように修正しました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public void outputNameList() { OutputStream outputStream = ...; // 初期化していることにします。 OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream); List <String> nameList = new ArrayList<String>(); nameList.add("太郎"); nameList.add("次郎"); nameList.add("花子"); try { writeNameList(nameList, outputStreamWriter); outputStreamWriter.close(); outputStream.close(); } catch (Exception e) { // ここは既存のコードで正しい例外処理が書かれてました } } private void writeNameList(List<String> nameList, OutputStreamWriter writer) throws IOException { for(String name : nameList) { writer.write(name); } } |
しかし、上記のサンプルコードでは例外が発生したときにcloseができていないので、リソースリークが起きます。
リソースリークを防ぐにはいついかなる場合でもcloseするようにしなければいけません。
これからは例外が発生した時も必ずcloseされるような実装を心がけたいです。
また、どこでcloseするかということもとても大事です。
サンプルコードは、呼び出し元で生成したOutputStreamWriterのインスタンスがwriteNameListメソッドに引数として渡されています。
この場合、OutputStreamWriterをcloseするのは呼び出し元かwriteNameListメソッドのどちらかでするか迷いますが、「インスタンスを生成した側で必ずcloseする」のが一般的なよい習慣です。
例えば、インスタンスを生成した側では他のメソッドでもOutputStreamWriterを使用する可能性があるからです。
呼び出したどこかのメソッドでcloseされていたら、呼び出し元が使おうと思っても使えないですよね?
closeが必要なインスタンスがあれば、「インスタンスを生成した側で必ずcloseする」と覚えておこうと思います。
実際の修正では、例外発生の有無に関わらず、try-with-resources構文を用いて、呼び出し元で自動的にcloseするようにしました。
修正後のサンプルコードは以下のようになりました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public void outputNameList() { //try-with-resources構文 try(OutputStream outputStream = ...; // 初期化していることにします。 OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream)){ List <String> nameList = new ArrayList<String>(); nameList.add("太郎"); nameList.add("次郎"); nameList.add("花子"); writeNameList(nameList, outputStreamWriter); } catch (Exception e) { // ここには適切な例外処理が書かれていました。 } } private void writeNameList(List<String> nameList, OutputStreamWriter writer) throws IOException { for(String name : nameList) { writer.write(name); } } |
3. 例外処理は catch して throw すれば良いとは限らない
以前の私は例外処理といえば catch して throw すれば良いと単純に考えていました。
しかし、それがどんな例外なのかを考えなければいけなかったと思います。
例外が発生したメソッドでは対処できない例外かもしれないですし、例外が発生したメソッドで対処するべきかもしれません。
今回のサンプルコードでは、呼び出し元に適切な例外処理が書いてあったため、throwすることがベストでした。
その例外が発生したメソッドでは対処出来ない場合は、throwする、できれば、どこでcatchされどのような例外処理をthrowした先でしているのかをこれからは確認しようと思います。
例外が発生したメソッドで対処するべき場合は、そのメソッドでどう例外処理をするのかを考えます。
どのログレベルなのかやログのメッセージをどうするかなど例外処理もさまざまです。
どんな例外処理をするかは開発現場のルールによるので、知らないのであれば確認することをおすすめします。
例えば、例外が発生したことをメールやチャットツールで通知することが例外処理の1つです。
まとめ
まず、例外処理はどんな例外が起きるのかを調べて、それに対してどの時点でどのように処理すれば適切かを考えて、実装することだと思います。
次に、冒頭で書いた今回学んだことをもう一度載せます。
- 例外を握り潰してはいけない
- 例外発生時にリソースリークが起こらないように注意する
- 例外処理は catch して throw すれば良いとは限らない
ここまで書いてきたしくじりから、私は以下を心がけようと思いました。
- 何かメソッドを使用する際はJavaドキュメントの例外欄を必ずチェックする
- 例外処理を書くときには適切なハンドリングを考えて、書いてみては先輩に確認してみる
例外処理をまったく理解していない状態でプログラムを書いていたと思うと背筋が凍りますね……。
しばらくはプログラムを読んでいるときに例外処理が書いてあったら、どういう例外処理なのかを考えてみる練習をしてみようと思います。