Fileを入出力するためのクラスを作ってみる
無事忌々しいテストも終わったもんだから
テスト期間中に組んだFileの入出力クラスをメモがてら紹介する
成り立ち
Javaから外部資源としてFileを読み書きするには
少々長ったらしい文を書かなくてはいけない
昔、Androidのクイズアプリを作った際に
CSVで問題データを保持させようということで
そのときに初めてファイルの入出力をいじった
非常に書きにくい
特に当時はリファレンスを見るという力も無かったため
色んな情報に左右されながら長ったらしいコードを書いていた
いま計画中のアプリでもCSVを使おうと思っているのでその前準備
何をするクラスか
大体Javaでファイルを扱うときは(ファイル管理ソフトでも作らない限りは)
新規作成・書き込み・読み込みこのぐらいができれば十分であろう
ついでにファイル削除もつけて、計4機能
こいつらを実装させていく
基本的にError処理は利用者任せにするため、全てthrow
使う機能
機能というと語弊があるかもしれないが気にせずに
今回のクラスはnio
のPaths
を使ってファイルを操作する
ファイルの読みはBufferedReader
、書きはPrintWriter
を使う
とりあえずフィールド変数
今回使用する変数の方々
/**
* ファイルの生成、削除、名前変更を担当
* */
private Path path;
/**
* 文字コード設定のための定数
* */
private static final Charset UTF_8 = Charset.forName("UTF-8");
private static final String BR = System.lineSeparator();
private static final Pattern p = Pattern.compile(BR);
/**
* 読み込み用
* */
private BufferedReader reader;
/**
* 書き込み用
* */
private PrintWriter writer;
private BufferedWriter bufferedWriter;
/**
* 読み込みの際に文字列連結高速化
* */
private StringBuilder content;
今回はちょっとJavaDocも書いてみた
ちょっと変わったところがこの二行
private static final String BR = System.lineSeparator();
private static final Pattern p = Pattern.compile(BR);
一行目は改行コードを取っているのだが
その改行コードがOSによって異なっているので
OSから改行コードをもらってきている
そのOS依存の改行コードを使ってパターンを作っているのが二行目
あとでこれを使ってゴニョゴニョする
今気づいたがあまりこれは意味がない・・・
続いてコンストラクタ
/**
* パスが文字列で与えられたときのコンストラクタ
* @param pathName 操作したいファイルのフルパスを渡す
* */
public FileManager(String pathName) throws IOException {
path = Paths.get(pathName);
init(true);
}
/**
* File型が与えられたときのコンストラクタ
* @param file 操作したいファイルのFile型を渡す
* */
public FileManager(File file) throws IOException {
path = file.toPath();
init(true);
}
/**
* Path型が与えられたときのコンストラクタ
* @param path 操作したいファイルのPath型を渡す
* */
public FileManager(Path path) throws IOException {
this.path = path;
init(true);
}
3種類の型に対応できるようにした
今頃File
という感じもするが
古いAPIだと返り値がFile
ということがあるらしい
init()の内容はその名の通り各フィールド変数の初期化
private void init() throws IOException {
bufferedWriter = Files.newBufferedWriter( path, UTF_8, StandardOpenOption.APPEND);
writer = new PrintWriter(bufferedWriter, true);
reader = Files.newBufferedReader( path, UTF_8 );
content = new StringBuilder();
}
ここで注意しておきたいのが
bufferedWriter = Files.newBufferedWriter( path, UTF_8, StandardOpenOption.APPEND);
StandardOpenOption.APPEND
をしっかりつけないと
実行時にファイルがまっさらになる
ほかは特に注意点なし
bufferedWriter = Files.newBufferedWriter( path, UTF_8, StandardOpenOption.APPEND);
reader = Files.newBufferedReader( path, UTF_8 );
こいつらは省エネな書き方
File file = new File(file_name);
BufferedWriter bw = new BufferedWriter(new FileWriter(file));
これが普通の書き方
最初に紹介したほうが個人的には好き
新規作成・削除
難しい処理は書くことなく、ほぼワンライン
無事に作成・削除できたかを知らせるboolean型の返り値を設定してある
/**
* ファイルを新規作成するためのメソッド
* @return 作成に成功すればTrue, 既にファイルが存在する場合はFalse
* @throws IOException 入出力エラーが発生した場合
* */
public boolean create() throws IOException {
return path.toFile().createNewFile();
}
/**
* ファイルを削除するためのメソッド
* @return 削除に成功すればTrue, 失敗すればFalse
* @throws IOException 入出力エラーが発生した場合
* */
public boolean delete() throws IOException {
Files.delete(path);
return !( exists() );
}
説明はDocに書いてある通り
exists()
っていうのは
ファイルが存在しているかどうかを調べるメソッド
以下の通りワンライン
public boolean exists() {
return Files.exists(path);
}
外からでも有無を確認できるようにpublicにしてある
ここからが本番、読み込み
ここからがようやく本番といった感じ
いかにに無駄なく素早く処理するか
素人ながら色々工夫をして書いた
/**
* ファイルの中身を読み込むためのメソッド
* @return ファイルの中身の文字列
* @throws IOException 入出力エラーが発生した場合
* @throws NullPointerException ファイルが空だった場合
* */
public String read() throws IOException, NullPointerException {
if (exists() && Files.isReadable(path)) {
String buffer;
do {
buffer = reader.readLine();
content.append( buffer ).append( BR );
} while (buffer != null);
return content.toString();
} else if ( !exists() ) {
System.err.println("Couldn't find File");
} else if ( !Files.isWritable(path) ) {
System.err.println("Have no authority to read");
}
return null;
}
最初のif文で存在しているか&読み込み権限があるかを見ている
あとは定型文
これを何回も書きたくないからこのクラスを作ったようなもの
if文でboolen型を!をつけてFalseの場合に反転させているが
== false
で書いたほうが読みやすい気はする
仕上げの書き込み
最後の難関
/**
* ファイルに書き込むためのメソッド
* @param text 書き込みたい内容
* @throws IOException 入出力エラーが発生した場合
* */
public void write(String text) throws IOException {
if (exists() && Files.isWritable(path)) {
if (p.matcher(text).find()) {
String[] buffer = text.split(BR, 0);
for (String line : buffer) {
writer.println(line);
}
} else {
writer.write(text);
}
writer.flush();
} else if ( !exists() ) {
System.err.println("Couldn't find File");
} else if ( !Files.isWritable(path) ) {
System.err.println("Have no authority to write");
}
}
ここで出てくるPattern
もし改行がある場合はそれを反映させ、ない場合はそのまま書き込む
この改行を楽にするためにPrintWriter
を使用している
とは言えどもBufferedWriter
にもnewLine()
なんてのがあるので
素直にBufferedWriter
を使えばよかった気もする
忘れられがちなClose
読み書きのメソッドを見て、close()がないことに気づかれただろう
忘れていたわけではなく、あえて書かなかったのだ
別にメソッドとして定義している
public void close_r() throws IOException {
reader.close();
}
public void close_w() throws IOException {
writer.close();
}
案の定のワンライン
なぜ読み書きの処理に埋め込まずに別途定義しているのか
それはそれぞれのメソッドがエラーを利用者に委ねているため
本来close()
をいうのはfinally
の中に書かなくてはいけない
しかし、エラーを投げてしまっているため
finally
もクソもないのである
そのため、苦肉の策として別途定義することで
利用者側にfinally
でしっかり閉めてもらうことができる
私自身、かなり嫌々この方式を取った
もし何か良い策があったら連絡下さい
最後に
作ってから気づく実際に使えるのか感
実践で使ったことがないので何とも言えないが
あまり使える気がしない
今度使った後に感想でも書くこととしよう
このコードはGithubに上げてある
以下のリンクからすべてを見ることができる
Github上では、今回紹介した以外のメソッドも幾つか書いてあるので
気になる方はぜひご一読を
以上
次はCSVのパーサーでも自作したい