Javaの例外処理で知らないと損する7つのテクニック:【改訂版】Eclipseではじめるプログラミング(24)(3/3 ページ)
これからプログラミングを学習したい方、Javaは難しそうでとっつきづらいという方のためのJavaプログラミング超入門連載です。最新のEclipseとJava 6を使い大幅に情報量を増やした、連載「Eclipseではじめるプログラミング」の改訂版となります(この回と前回のみ、別連載「EclipseでJavaに強くなる」の改訂版です。今回は第4回Javaの例外のテクニックを知る」の改訂版です)
【5】読み書きでfinallyを使う際の注意点
ファイルデータを読み書きをする処理では、入力ストリーム「in」と出力ストリーム「out」を用意して、データを読み込みながら書き込みをするといったことをしたいときがあります。このとき、finallyで次のように2つのリソースについてcloseするようなことをしがちなので、気を付けましょう。
//略 void exec() throws Exception { //略 } finally { in.close(); out.close(); } }
この場合、せっかくfinallyでinとoutの両方のリソースを解放しようとしていますが、「in.close();」でエラーが発生すると、「out.close();」が実行されないので、out関係のリソースについては解放されずにメモリリークなどの原因となります。
対応方法は「変数へ全部のデータを一時的に保存してから、書き込みをする」とか、「リソースを確保した順と逆順に確実にfinallyでリソースを解放する」ということになります。
変数へ全部のデータを一時的に保存してから、書き込みをする
前者の場合は次のようなプログラムを書くことになります。「src/sample24/SampleApp.java」の内容をコピーして、「src/sample24/SampleApp.txt」を書き出しています。
package sample24; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.FileReader; import java.io.FileWriter; public class SampleApp3 { public void execute() throws java.io.IOException { java.util.List lines = new java.util.LinkedList(); BufferedReader in = null; try { in = new BufferedReader(new FileReader("src/sample24/SampleApp.java")); String line = null; while ((line = in.readLine()) != null) { lines.add(line); } } finally { if (in != null) { in.close(); // inはここで確実にclose } } BufferedWriter out = null; try { out = new BufferedWriter(new FileWriter("src/sample24/SampleApp.txt")); for (String line : lines) { out.write(line); out.newLine(); } out.flush(); } finally { if (out != null) { out.close(); // outはここで確実にclose } } } public static void main(String[] args) { SampleApp3 app = new SampleApp3(); try { app.execute(); } catch (java.io.IOException e) { e.printStackTrace(); // 読み込みか書き込みでエラー } } }
データ読み込みに当たっては、BufferedReader型の変数inを用意し、これを使ったらfinally節で確実にcloseしています。データ書き込みに当たっては、BufferedWriter型の変数outを用意し、これを使ったらfinally節で確実にcloseしています。
リソースを確保した順と逆順に確実にfinallyでリソースを解放する
「リソースを確保した順と逆順に確実にfinallyでリソースを解放する」という場合は、次のようになります。
package sample24; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.FileReader; import java.io.FileWriter; public class SampleApp4 { public void execute() throws java.io.IOException { BufferedReader in = null; try { in = new BufferedReader(new FileReader("src/sample24/SampleApp.java")); BufferedWriter out = null; try { out = new BufferedWriter(new FileWriter("src/sample24/SampleApp.txt")); String line; while ((line = in.readLine()) != null) { out.write(line); out.newLine(); out.flush(); } } catch (java.io.IOException e) { // エラーが発生する場所は複数ある // new FileWriter() // new BufferedWriter() // in.readLine() // out.write(), out.newLine(), out.flush() e.printStackTrace(); } finally { if (out != null) { out.close(); // outはここで確実にclose } } } catch (java.io.IOException e) { // new FileReader, new BufferedReader でエラー か // out.close() でエラー e.printStackTrace(); } finally { if (in != null) { in.close(); // inはここで確実にclose } } } public static void main(String[] args) { SampleApp4 app = new SampleApp4(); try { app.execute(); } catch (java.io.IOException e) { e.printStackTrace(); // in.close()でエラー } } }
ネストが深くなるため、メソッドに分けて実装することが多いのですが、処理の流れ全体を理解するには一覧性を優先した方がよいので、1つのメソッドにしています。in、out、それぞれについてtryとfinallyを対応させている点、finallyで発生する例外をキャッチできるようにしている点に注目をしてください。
「リソースを確保した順に確実にリソースを解放していく」というのは基本といえば基本なのですが、処理が複雑になってくると、意外と見落としやすくなるため、経験豊かな開発者でもミスをしてしまいます。その結果、リソース解放がされないプログラムを書いてしまうことがあるので、気を付けましょう。
【6】コンパイル時にチェックされる例外を使いたくない場合
例外を使うと、エラー処理に対して確実にコーディングができるようになり、プログラムの見通しも良くなりますが、「コンパイル時にチェックされる例外」を使用するには下記の3つの注意点も考慮する必要があります。
- あるメソッドでjava.sql.SQLExceptionを投げるように設計すると、データベースを使っていることが分かってしまう→実装の詳細が分かってしまう
- メソッドが投げる例外を変更すると、例外をキャッチしているコードもすべて影響を受けてしまう。開発中の場合は、投げる例外を細分化したり統合したりと変更したくなることが特に多いが、「コンパイル時にチェックされる例外」の変更は影響範囲が大きいため気軽に例外の設計を変更できない
- メソッドが投げる例外が多過ぎて、本来2、3行程度の単純なコードが10以上のcatch節を持つコードが必要になる場合がある。これにより、コードの可読性が著しく低下することがある
このように、「コンパイル時にチェックされる例外」であるjava.lang.Exceptionクラスを継承する独自の例外クラスでは強制力があることにより、使いにくい面もあります。従って、場合によってはjava.lang.RuntimeExceptionクラスを継承した独自の例外クラス設計を検討する価値はあります。
ただし、このクラスを使用する場合には、どのメソッドがどんな例外を投げるのかについての情報を完全にドキュメント化しておく必要があります。java.lang.Exceptionクラスのサブクラスであればコンパイラがチェックをしてくれますが、java.lang.RuntimeExceptionクラスのサブクラスについては、そういったチェックがされないので、ドキュメントしか頼りになるものがなくなってしまうからです。
【7】例外処理を有効に使うための心構え
以上のように、例外を使うとエラー処理をうまくコーディングできます。よく理解して使えるようになってください。7つ目は広い意味でのテクニックとして心構えを紹介しておきます。
エラー処理の方法を考えよう
また、本文では説明をしませんでしたが、メソッドであればエラーコードなども返せますが、コンストラクタではそういった処理をコーディングできません。インスタンス生成時のエラー発生を捕捉するには例外は必須です。
コンストラクタでのエラー捕捉には例外が必須ですが、そうでない場合は、例外はあくまでエラーへ対応するための1つの手段でしかありません。アプリケーション全体を考えた場合は例外を使うよりも、メソッドでエラーコードを返すようにしてエラー処理をした方がいい場合もあります。どういった方法がいいのかを常に考えながらコーディングするようにしましょう。
例外処理本来の目的以外で使用しない
また、例外を使うと処理の流れを変えられるので、条件文のような使い方も可能です。しかし、例外はその名のとおり、「例外的な処理が発生したことを通知する、その通知を捕捉する」という目的で設計されています。処理の流れを変えるために例外を使うのは何のメリットもないので、例外処理本来の目的以外で使用しないようにしましょう。
Java SE 7では、リソースに対してtry文の対応付けが可能に
複数のリソースを取り扱うときには、リソースの解放をfinally節で確実に実施するように意識すると、メモリリークの発生しないプログラムが作れるようになるはずです。有効に利用しましょう。ちなみに、今年正式リリースされたJava SE 7では、リソースに対してtry文の対応付けが可能になったので、簡単になっています。興味のある読者は調べてみてください。
こういった点に注意しながら、例外処理の仕組みをよく理解したうえで有効に利用できるようになってください。 今回作ったサンプルのソースコードは、こちらからダウンロードできます。
筆者紹介
小山博史(こやま ひろし)
情報家電、コンピュータと教育の研究に従事する傍ら、オープンソースソフトウェア、Java技術の普及のための活動を行っている。長野県の地域コミュニティである、SSS(G)やbugs(J)の活動へも参加している。
著書に「基礎Java」(インプレス)、共著に「Javaコレクションフレームワーク」(ソフトバンククリエイティブ)、そのほかに雑誌執筆多数。
- Javaの例外処理で知らないと損する7つのテクニック
- プログラマの宿命! 例外とエラー処理を理解する
- いまさら聞けない「Javadoc」と「アノテーション」入門
- 7ステップで理解するJavaでの列挙型/enum使用法
- 拡張for文の真の実力を知り、反復処理を使いこなせ
- キュー構造をJavaで実装してジェネリック型を理解する
- 強く型付けされているJavaの理解に必修の“型変換”
- あなたの知らない、4つのマニアックなJava文法
- “ネスト”した型で始める軽量Javaプログラミング!?
- Javaは「抽象クラス」で実装を上手に再利用できる
- 再利用性の高いクラス作成に重要な“アクセス制御”
- “コンストラクタ”と初期化、本当に理解できてる?
- 継承やオーバーライドで簡単にクラスを“拡張”しよう
- 「static」でクラス共有の変数・メソッドを使いこなせ!
- Javaの実案件に必須のパッケージとインポートを知る
- プログラムを「変更」しやすくする“インターフェイス”
- Javaの参照型を文字列操作で理解して文法を総復習
- クラスの振る舞いを表すJavaの“メソッド”とは?
- 複雑なデータを表現できるクラスやフィールドって?
- データ集合を扱うのに便利なJavaの配列と拡張for文
- プログラミングの真骨頂! Javaで“反復処理”を覚える
- プログラミングの醍醐味! Javaで“条件式”を理解する
- Javaで一から理解するプログラムの変数と演算子
- Eclipse 3.4で超簡単Javaプログラミング基礎入門
Copyright © ITmedia, Inc. All Rights Reserved.