では、ファイルコピーの実装例を見ていきましょう。サンプルの行数を減らすために、仕様に下記の制約を課しています。
- ファイルからファイルへのコピーのみとする(ディレクトリからディレクトリへのコピーや、ファイルからディレクトリへのコピーは仕様から除く)
- 出力先ファイルがすでに存在する場合には、コピーを中断する
基本的な構造は下記のようになります。
- ファイルからストリームを作る
- ストリームをコピーする
しかし、この仕様に加えて、ファイルの存在確認などの各種入力チェックが実装されていることが分かります。
先ほどの例と比べても、さらに入力チェック処理が増えていることが分かります。入出力処理において入力チェック処理が複雑になるのは、“宿命”ともいえます。この傾向は、ある意味プログラマーの常識の1つでさえあります。
ファイルをコピーするサンプル |
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class CopySample {
public static void main(final String[] args) {
// パラメータのディレクトリ名は適宜書き替えてください
new CopySample().copy("sampledata.txt", "bbb.out");
}
public void copy(final String argInputFile
,final String argOutputFile) {
if (argInputFile == null) {
throw new IllegalArgumentException("パラメータに"
+"与えられた処理対象入力ファイルはnullでした。"
+"しかし、ここにnullを与えることはできません。");
}
if (argOutputFile == null) {
throw new IllegalArgumentException("パラメータに"
+"与えられた処理対象出力ファイルはnullでした。"
+"しかし、ここにnullを与えることはできません。");
}
final File targetInputFile = new File(argInputFile);
final File targetOutputFile = new File(argOutputFile);
if (targetInputFile.exists() == false) {
throw new IllegalArgumentException("パラメータに"
+"与えられた処理対象入力ファイル["+ argInputFile
+"]は存在しません。処理中断します。");
}
if (targetOutputFile.exists()) {
throw new IllegalArgumentException("パラメータに"
+"与えられた処理対象出力ファイル["+ argOutputFile
+"]はすでに存在します。処理中断します。");
}
if (targetInputFile.isFile() == false) {
throw new IllegalArgumentException("パラメータに"
+"与えられた処理対象入力ファイル["+ argInputFile
+"]は実際にはファイルではありません。");
}
System.out.println("ファイルコピーサンプル。");
System.out.println("処理対象入力ファイル["
+ argInputFile + "]。");
System.out.println("処理対象出力ファイル["
+ argOutputFile + "]。");
// ここに実際の処理を書く
try {
copy(targetInputFile, targetOutputFile);
System.out.println("ファイルのコピーは終了しました。");
} catch (IOException e) {
System.out.println(
"ファイルのコピー処理中に例外が発生しました。"
+ "処理中断します。:" + e.toString());
e.printStackTrace();
return;
}
}
public void copy(final File argInput, final File argOutput)
throws IOException {
if (argInput == null) {
throw new IllegalArgumentException("パラメータに"
+"与えられた入力ファイルはnullでした。"
+"しかし、ここにnullを与えることはできません。");
}
if (argOutput == null) {
throw new IllegalArgumentException("パラメータに"
+"与えられた出力ファイルはnullでした。"
+"しかし、ここにnullを与えることはできません。");
}
InputStream inStream = null;
OutputStream outStream = null;
try {
inStream = new BufferedInputStream(
new FileInputStream(argInput));
outStream = new BufferedOutputStream(
new FileOutputStream(argOutput));
copyStream(inStream, outStream);
// 最後にフラッシュをします
outStream.flush();
} finally {
if (inStream != null)
inStream.close();
if (outStream != null)
outStream.close();
}
// ファイルの最終更新日時をコピー。
argOutput.setLastModified(argInput.lastModified());
}
public void copyStream(final InputStream inStream
,final OutputStream outStream)throws IOException {
if (inStream == null) {
throw new IllegalArgumentException("パラメータに"
+"与えられた入力ストリームはnullでした。"
+"しかし、ここにnullを与えることはできません。");
}
if (outStream == null) {
throw new IllegalArgumentException("パラメータに"
+"与えられた出力ストリームはnullでした。"
+"しかし、ここにnullを与えることはできません。");
}
final byte[] byteBuf = new byte[8192];
for (;;) {
final int length = inStream.read(byteBuf);
if (length < 0) break;
outStream.write(byteBuf, 0, length);
}
}
} |
|
先ほどと同様、参考までにこの処理のためのJUnitソースコードを示します。ここでは、主な異常系を中心に、しかもなるべく簡潔に記述してある点に注意してください。現実的なJUnitコードはこれよりずっと多くのテストコードによって実現されるものです。
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import junit.framework.TestCase;
public class CopySampleTest extends TestCase {
public void testCopyStringString() throws Exception {
try {
new CopySample().copy(null, "bbb");
fail("nullを与えた場合には"
+"例外が発生しなくてはなりません。");
} catch (IllegalArgumentException ex) {
}
try {
new CopySample().copy("aaa", null);
fail("nullを与えた場合には"
+"例外が発生しなくてはなりません。");
} catch (IllegalArgumentException ex) {
}
try {
new CopySample().copy("ccc.out", "bbb.out");
fail("入力ファイルが存在しない場合には"
+"例外が発生しなくてはなりません。");
} catch (IllegalArgumentException ex) {
}
try {
new CopySample().copy(".", "bbb.out");
fail("入力ファイルが実はディレクトリの場合には"
+"例外が発生しなくてはなりません。");
} catch (IllegalArgumentException ex) {
}
// このJUnitコードの2度目以降の実行時に
// 例外が発生するのを防ぐ記述
new File("bbb.out").delete();
new CopySample().copy("sampledata.txt", "bbb.out");
}
public void testCopyFileFile() throws Exception {
try {
new CopySample().copy(null, new File("."));
fail("nullを与えた場合には"
+"例外が発生しなくてはなりません。");
} catch (IllegalArgumentException ex) {
}
try {
new CopySample().copy(new File("."), null);
fail("nullを与えた場合には"
+"例外が発生しなくてはなりません。");
} catch (IllegalArgumentException ex) {
}
}
public void testCopyStream() throws Exception {
try {
new CopySample().copyStream(null
, new ByteArrayOutputStream());
fail("nullを与えた場合には"
+"例外が発生しなくてはなりません。");
} catch (IllegalArgumentException ex) {
}
try {
new CopySample().copyStream(new ByteArrayInputStream(
"テスト"
.getBytes()), null);
fail("nullを与えた場合には"
+"例外が発生しなくてはなりません。");
} catch (IllegalArgumentException ex) {
}
final ByteArrayOutputStream outStream
= new ByteArrayOutputStream();
new CopySample().copyStream(new ByteArrayInputStream(
"テスト".getBytes()),
outStream);
outStream.flush();
final String output = new String(outStream.toByteArray());
assertEquals("テスト", output);
outStream.close();
}
} |
|
ここまで紹介してきたもの以外の主要な注意点について、ここで簡単に紹介します。
ストリームで入出力を行う場合には、ストリームのクローズ(closeメソッド呼び出し)忘れをしないように注意しましょう。特に、ファイルに対するストリームのクローズ忘れをしてしまうと、厄介な現象が発生してしまう場合があります。
また、正常系では、きっちりクローズするようにプログラムが書かれていても、例外発生時などの異常系においてストリームのcloseメソッドを呼ばなくなるような実装間違いも、結構ありがちなものです。finallyブロックを活用するなどしてクローズ忘れをきっちりと防止するような実装を心掛けて、これに対処しましょう。
バイト出力ストリーム(java.io.OutputStream)の場合には、ストリームをcloseする前にflushメソッド呼び出しを明示的に呼び出す必要があります。こちらを呼び忘れると、データの終わりの部分がストリームに書き込まれない現象が発生する場合があります。
なお、文字出力ストリーム(java.io.Writer)の場合には、closeメソッドの処理の中でフラッシュを行うようになっています。closeメソッドを確実に呼び出しさえすればよいのです。詳しくは、Java APIのドキュメントをご参照ください。
Webアプリケーションなどの同時に複数のプログラムが動作する環境でファイル入出力を行う際には、「排他をどのように実現するのか」などといった同時にプログラムが動作する環境に起因する問題に対する留意が必要です。この点も結構難しい内容となります。
ファイル操作に関する基本的なことについて紹介してきましたが、いかがだったでしょうか?
また、ファイルの入出力処理は何かとチェック系の処理が増える傾向にあり、今回紹介した以外の、例えば入出力対象データの内容妥当性のためのチェック処理などを追加し始めると、記述内容はさらに複雑になります。
一方で、例えばCSVファイルの入出力処理を現実的なシステム開発で実装する場合には、何かしら既存のツールやライブラリを活用することにより、この傾向を改善しようという取り組みも多く行われています。このようなツールの1つとして、手前みそながら、blancoCsvという設計書からデータ入出力のチェック処理を自動生成するツールを紹介しておきます。
このように、ファイル操作は難しいものの1つですが、バイトストリームや文字ストリームといった入出力処理ならではの技術を理解できていれば、今後のプログラミングに弾みがつくことが期待できます。少しずつなじんでいって、いつかはマスターしてしまいましょう。