ファイルへの書き出し
では、実際にファイルアクセスの処理がどうなるか説明していきましょう。まずは、ファイルへの書き出しについてです。ここでは、「data.txt」というテキストファイルに「This is Test.」と書き出す処理を考えてみましょう。
public static void main(String[] args) { FileWriter writer = new FileWriter("data.txt"); BufferedWriter bwriter = new BufferedWriter(writer); bwriter.write("This is Test."); bwriter.close(); }
クラスのmainメソッドのみ掲載をしておきます。実をいえば、このメソッドには問題があるのですが、ファイルへの書き出しの基本処理手順として考えてください。
まず最初に、FileWriterのインスタンスを作成します。このクラスは、引数に書き出すファイルのパスを指定することで、そのファイルに書き出すためのインスタンスを作成するようになっています。
変数 = new FileWriter( ファイルパス );
これでOKです。ここでは、「data.txt」というファイルに書き出すようにしていたわけです。インスタンスの作成そのものは実に簡単です。
続いて、BufferedWriterインスタンスの作成です。BufferedWriterは、引数にWriterインスタンスを渡すことで、そのWriterにバッファ機能を付加したインスタンスを生成します。
変数 = new BufferedWriter( [Writerインスタンス] );
こんな具合に、「基本となるWriterをくるんで新しいBufferdWriterを作る」といった操作を行います。ファイルアクセス関係では、このように「あるインスタンスを作り、そのインスタンスをもとに新しいインスタンスを作り、それをもとに……」というようにして必要なインスタンスを作り出していく操作をよく行います。
こうしてインスタンスの準備ができたら、そのメソッドを呼び出してテキストの書き出しを行います。
[Writer].write( String値 );
これでテキストをファイルに書き出すことができました。最後にBufferedWriterを閉じて作業終了です。
[BufferedWriter].close();
このcloseは、「フラッシュ」といってバッファに残っている情報をすべて強制的に書き出す処理を行った後、ファイルを開放します。またBufferedWriterはFileWriterを内包して作られていますから、これをcloseすればFileWriterも閉じられますので、改めてFileWriterを閉じる必要はありません。
Writer/BufferedWriterを使ったファイルへの書き出しは、これだけです。必要に応じて、writeメソッドを何度か繰り返し実行したりすることはあるでしょうが、原則としてこれ以上に理解することはありません。
例外の発生
しかしながら、このメソッドは、実はコンパイルできないのです。なぜなら「例外処理」がなされていないからです。
今まで、その必要がなかったので特に取り上げてきませんでしたが、これらのファイル関係のクラスでは、「メソッドの実行に失敗する」ということがあります。例えば、ファイルがロックされていたり使用中で書き出しできないような場合もあるでしょう。こうしたとき、Javaでは「例外(Exception)」というものが発生します。
例外は、一般にいわれる「エラー」に相当するものです。プログラムを実行しているときに何かの異常があると、Javaでは例外クラスのインスタンスが発生します。これによって、発生したエラーの内容を調べたり、それへの対応を行ったりするようになっているのです。「エラーが起きたらプログラムが強制終了しておしまい」では、あまりに問題です。致命的なエラーでない限りは、何らかのリカバリー処理をしてプログラムの実行を続けられるようにしておくべきです。そのために用意されるのが例外処理です。
例外処理は、次のような形で記述されます。
try { ……例外が発生する可能性のある処理…… } catch( 例外クラスの引数 ){ ……例外発生時の処理…… } finally { ……構文を抜ける際に実行する処理…… }
tryの後にある{}内に、例外が発生する可能性のある処理を記述しておきます。ここで例外が起こると、その後にある「catch」というところにジャンプします。このとき、発生した例外インスタンスが、その後の()内の引数に渡されます。そして、その後にある{}部分の処理を実行して例外からのリカバリーを行うわけです。
また、その後にある「finally」は、この構文を抜けるときに必ず実行させる処理を記述するためのものです。これは、例外の発生の有無に関係なく、必ず実行されます。ただし必須というわけではなく、不要なら省略することもできます。
分かりにくいのは「例外クラスは、一体どういうものか」ということでしょう。例外クラスは、Exceptionというクラスあるいはこれを継承したクラスになります。例外が発生する可能性を持つメソッドなどでは、それぞれの例外の内容に応じて専用の例外クラスを用意し、それを送るようになっているのです。
この例外処理というのは頭で考えてもなかなか理解しづらいところがありますから、実例を示しましょう。先ほどのメソッドを例外処理を適用した形に書き直してみます。
public static void main(String[] args) { try { FileWriter writer = new FileWriter("data.txt"); BufferedWriter bwriter = new BufferedWriter(writer); bwriter.write("This is Test."); bwriter.close(); } catch (IOException e) { e.printStackTrace(); } }
実は、これでもまだ問題があるのですが、try構文の基本的な書き方としてみてください。ここでは、new FileWriter、new BufferedWriter、write、closeという4つのメソッドをそのままtry内に書いてあります。この4メソッドのうち、new BufferedWriter以外のものはすべて例外を発生させる可能性を持っています。従って、こんな具合にすべてtry内に入れておかないといけないわけです。このtry内には、例外を発生させないメソッドも書いておけます。
発生する例外クラスとcatchの関係
これらのメソッドで発生するのは「IOException」という例外クラスです。I/O関係で発生する例外として用意されているもので、ファイルアクセスでは、発生するのは大体この例外だけと考えてもよいでしょう。
発生した例外を受け止めるcatchというところでは、受け止める例外クラスが引数に指定されます。ここでは、IOExceptionが指定されています。これにより、このIOExceptionクラスおよびこのサブクラスの例外は、すべてこのcatchで受け止めるようになります。
重要なのは「引数で指定された例外以外のものは、そのcatchでは受け止めない」という点です。ですから、いくつもの異なる例外が発生するような場合には、
……実行する処理……
} catch (○○Exception e){
……例外処理……
} catch (××Exception e){
……例外処理……
}
こんな具合に、catchをいくつも続けて書くこともあります。こうすると、発生した例外の種類ごとに異なる処理を行わせたりすることもできるのです(これについては後述します)。
さて、ここでの例では、例外発生時に実行する処理は以下のようになっています。
e.printStackTrace()
このprintStackTraceというメソッドは、この例外オブジェクトのバックトレースを出力するものです。バックトレースというのは、catchされたときに例外が発生した地点(実行中のメソッド)を、それが呼び出されているメソッドを順に調べては出力する、という形でトレースしていくものです。「どこで、何が起こったか」を知る上で、バックトレースの情報は非常に重要なのです。このため、例外時にはこのprintStackTraceを呼び出してバックトレースを標準出力に書き出しておくことが多いです。
closeを更にtryで囲む
さて、これで完成!…かというとそうではありません。ここでは、tryの中にすべての処理を入れてあります。となると、途中で例外が発生したら、その時点でcatchに飛んでしまいますから、その後の処理は実行されないことになってしまいます。これは問題です。特に、ファイルをopenしてあるのに、最後のcloseをしないままプログラムが終わってしまう、というのは避けないといけません。
そこで、このclose処理だけを「finally」のところに移動して、例外が発生するしないに関わらず、必ずcloseをしてから終えるように修正をしましょう。
public static void main(String[] args) { BufferedWriter bwriter = null; try { FileWriter writer = new FileWriter("data.txt"); bwriter = new BufferedWriter(writer); bwriter.write("This is Test."); } catch (IOException e) { e.printStackTrace(); } finally { try { bwriter.close(); } catch (IOException e) { e.printStackTrace(); } } }
finallyは、try部分とは別の領域になるので、tryで宣言した変数は使えません。ですから、try構文に入る前にBufferedWriterの変数を宣言しておき、try部分でもfinally部分でも使えるようにしておく必要があります。
また、closeをfinallyに移動するとき、もう1つ注意しないといけないのは「closeも例外を起こす」ということです。finally部分は、try部分ではありませんから、ここで例外が起こるとcatchには飛びません。そこで、この中に更にtry構文を用意してcloseを書いてやる必要があります。
なんだかずいぶんとややこしくなってしまいましたが、これでようやくまともに使える書き出し処理が完成しました。
