プログラミングでは、何らかのデータを読み出したり、書き出したりすることが数多くあります。
データの保存先は、データベースが用いられることが一般的ですが、まとまった文章や構造化されていないデータなどはファイルで保存されることも多くあります。
そのため、ファイル操作はプログラミングを学習する中で重要項目の1つとなります。
本記事では、Javaでよく使われるファイル操作について解説します。
プログラミング初心者の方の学習や、忘れてしまった方の復習として、参考にしていただければ幸いです。
記載しているプログラムは、Java11を使って動作確認をしています。
Javaのファイル操作
Javaのファイル操作として、以下の内容を採り上げます。
- ファイル操作を扱うパッケージ
- ファイルの読み込み
- ファイルの書き込み
- ファイルの削除
- ファイルの存在確認
- ファイルのコピー
- ディレクトリの一覧取得
- ディレクトリの作成
- ディレクトリの削除
ファイル操作を扱うパッケージ
Javaのファイル操作は、Java6以前は java.io パッケージ配下のクラスを利用することが多かったですが、Java7で java.nio パッケージが作成され、便利に扱えるクラスが増えました。
そのため、特別な理由がなければ、java.nioパッケージのクラスを利用することをおすすめします。
本記事でも java.nio パッケージを使ったファイル操作を中心に解説していきます。
ファイルの読み込み
ファイルを読み込むには、Filesクラスの readString を使います。
try { Path path = Paths.get("./files/testfile.txt"); String s = Files.readString(path); System.out.println(s); } catch (IOException e) { System.out.println(e); }
上記のプログラムでは、filesディレクトリのtestfile.txtファイルを読み込んで、内容をコンソールに出力しています。
ファイルが存在しないなどにより、読み込みに失敗した場合は、例外が発生し、catch文で例外情報をコンソールに出力しています。
この方法では、ファイルの中身を一度にすべて読みこんでしまうため、大きなファイルだと時間がかかったり、メモリを大量に消費してしまいます。
ファイルを1行ずつ読み込むには、Filesクラスの lines でストリーム形式にして読み込みます。
try (Stream s = Files.lines(Paths.get("./files/testfile.txt"))){ s.forEach(System.out::println); } catch (IOException e) { System.out.println(e); }
上記のプログラムでは、ストリーム形式で1行ずつ読み込み、forEachによってコンソールに出力しています。
ストリームは最後に閉じる必要がありますので、try-with-resources を使ってストリームを定義しています。
ファイルの書き込み
ファイルの書き込みは、新規にファイルを作成して書き込む方法と、既存のファイルに追記する方法があります。
新規ファイルへの書き込み
新規にファイルを作成して書き込むには、Filesクラスの writeString を使います。
try { String s = "新規にファイルを作成するテスト"; Path path = Paths.get("./files/writefile.txt"); Files.writeString(path, s); } catch (IOException e) { System.out.println(e); }
上記のプログラムでは、filesディレクトリにwritefile.txtというファイルを作成し、その中に「新規にファイルを作成するテスト」という文字列を書き込んでいます。
読み込みの時と同じように、一度にすべて書き込んでしまうため、大量のデータを書き込むにはあまり効率が良いと言えません。
ストリームを使って書き込むには、BufferedWriterクラスを使います。
try(BufferedWriter bw = Files.newBufferedWriter(Paths.get("./files/writefile.txt"))) { String s = "ストリームを使って新規にファイルを作成するテスト"; bw.write(s); } catch (IOException e) { System.out.println(e); }
上記のプログラムでは、writeメソッドを呼出すたびに書き込みを行います。大量のデータがある場合は分割してwriteメソッドを呼出すと効率よく処理を行えます。
上記のどちらのプログラムも実行後にファイルが作成されますが、毎回新規にファイルを作成するため、何度実行してもファイルの中身は同じ結果になります。
既存ファイルへの追記
既存のファイルへ追記するには、writeString の引数にAPPENDオプションを指定します。
try { String s = "既存にファイルに追記するテスト"; Path path = Paths.get("./files/writefile.txt"); Files.writeString(path, s, StandardOpenOption.APPEND); } catch (IOException e) { System.out.println(e); }
上記のプログラムでは、filesディレクトリのwritefile.txtというファイルに、「既存ファイルに追記するテスト」という文字列を追記しています。
デフォルトでは、既存ファイルが存在しない場合はエラーとなります。エラーとしたくない場合には、さらにオプションを追加します。
try(BufferedWriter bw = Files.newBufferedWriter( Paths.get("./files/writefile.txt"), StandardOpenOption.APPEND, StandardOpenOption.CREATE)) { String s = "ストリームを使って既存ファイルに追記するテスト"; bw.write(s); } catch (IOException e) { System.out.println(e); }
ファイルの削除
ファイルを削除するには、Filesクラスの delete を使います。
try { Path path = Paths.get("./files/writefile.txt"); Files.delete(path); } catch (IOException e) { System.out.println(e); }
上記のプログラムでは、filesディレクトリのwritefile.txtというファイルを削除します。
ファイルが存在しない場合は、例外が発生します。
ファイルが存在する場合に削除する場合は、deleteIfExists を使います。
ファイルが存在しない場合は、エラーになりません。
try { Path path = Paths.get("./files/writefile.txt"); Files.deleteIfExists(path); } catch (IOException e) { System.out.println(e); }
ファイルの存在確認
ファイルが存在するかどうかを確認するには、Filesクラスの exists を使います。
System.out.println(Files.exists(Paths.get("./files/testfile.txt"))); // -> true System.out.println(Files.exists(Paths.get("./files/notexists.txt"))); // -> false System.out.println(Files.exists(Paths.get("./files"))); // -> true
exsitsは、ファイルが存在する場合はtrueを返し、存在しない場合はfalseを返します。
ディレクトリに対しても存在確認をすることができます。
ファイルかディレクトリかを判別するには、Filesクラスの isRegularFile または isDirectory で確認します。
System.out.println(Files.isRegularFile(Paths.get("./files/testfile.txt"))); // -> true System.out.println(Files.isDirectory(Paths.get("./files/testfile.txt"))); // -> false System.out.println(Files.isRegularFile(Paths.get("./files"))); // -> false System.out.println(Files.isDirectory(Paths.get("./files"))); // -> true
ファイルのコピー
ファイルをコピーするには、Filesクラスの copy を使います。
try { Path src = Paths.get("./files/testfile.txt"); Path dst = Paths.get("./files/copyfile.txt"); Files.copy(src, dst); } catch (IOException e) { System.out.println(e); }
上記のプログラムでは、filesディレクトリにcopyfile.txtを作成しますが、すでに同ファイルが存在している場合は、上書いてコピーします。
copyはファイルの内容はコピーされますが、作成日時や変更日時などのメタデータはコピーしません。これらもコピーするにはオプションを指定します。
try { Path src = Paths.get("./files/testfile.txt"); Path dst = Paths.get("./files/copyfile.txt"); Files.copy(src, dst, StandardCopyOption.COPY_ATTRIBUTES); } catch (IOException e) { System.out.println(e); }
ディレクトリの一覧取得
ディレクトリのファイル一覧を取得するには、Filesクラスの list を使います。
listは、ファイル名やディレクトリ名のPathをストリームで返します。
try (Stream s = Files.list(Paths.get("./files"))){ s.forEach(System.out::println); } catch (IOException e) { System.out.println(e); }
上記のプログラムでは、filesディレクトリのファイルとディレクトリが取得され、その配下のファイルやディレクトリは取得されません。
配下のすべてのファイルやディレクトリを取得するには、Filesクラスの walk を使います。
try (Stream s = Files.walk(Paths.get("./files"))){ s.forEach(System.out::println); } catch (IOException e) { System.out.println(e); }
取得するファイルやディレクトリの条件を指定したい場合には、上記の結果から絞り込むこともできますが、find を使う方法もあります。
try (Stream s = Files.find(Paths.get("./files"), Integer.MAX_VALUE, (p, a) -> a.isRegularFile())){ s.forEach(System.out::println); } catch (IOException e) { System.out.println(e); }
findメソッドの第3引数に指定する p(Path) と a(BasicFileAttrbutes) を使って条件を指定します。
上記のプログラムでは、ファイルのみを対象とし、ディレクトリを除外しています。
ディレクトリの作成
ディレクトリを新規に作成するには、Filesクラスの createDirectory を使います。
try { Path path = Paths.get("./files/testdir1"); Files.createDirectory(path); } catch (IOException e) { System.out.println(e); }
上記のプログラムでは、filesディレクトリの配下にtestdir1ディレクトリを作成しています。
すでにtestdir1ディレクトリが存在する場合にはエラーとなります。
複数階層のディレクトリを一気に作成するには、createDirectories を使います。
すでにディレクトリが存在している場合でもエラーになりません。
try { Path path = Paths.get("./files/testdir2/testdir2"); Files.createDirectories(path); } catch (IOException e) { System.out.println(e); }
プログラムの中では、一時的にファイルを扱うためにディレクトリを作成したい場合があります。
プログラムの特性にもよりますが、基本的には他のプログラムや処理で使うディレクトリとは異なる専用のディレクトリを作るのが一般的です。
そのような用途の場合には、ランダムな名称でディレクトリを作成してくれる createTempDirectory を使います。
try { Path path = Paths.get("./files/"); Path tempdir = Files.createTempDirectory(path, "prefix-"); System.out.println(tempdir.toString()); // -> ./files/prefix-10890748978662597083 } catch (IOException e) { System.out.println(e); }
createTempDirectoryメソッドの第2引数で接頭辞となる文字列を指定します。
戻り値として、作成したディレクトリのPathを返します。
ディレクトリの削除
ディレクトリを削除するにはファイルの削除と同じく、Filesクラスの delete を使います。
try { Path path = Paths.get("./files/testdir1"); Files.delete(path); } catch (IOException e) { System.out.println(e); }
削除対象のディレクトリに、ファイルやディレクトリが存在する場合はエラーとなります。
ディレクトリとファイルを一括で削除したい場合は、walk と delete を組み合わせて使います。
try (Stream s = Files.walk(Paths.get("./files/testdir3"))){ s.sorted(Comparator.reverseOrder()).forEach(p -> { try { Files.delete(p); } catch (IOException e) { System.out.println(e); } }); } catch (IOException e) { System.out.println(e); }
上記のプログラムでは、testdir3ディレクトリを配下のファイルやディレクトリとともに一括で削除します。
中間でsortしているのは、パスの長い順(末端のファイルから優先)に削除していくためです。
複数の処理で1つのファイル操作を行う場合は要注意
ファイルはデータを保存しておく場所として使いやすいのですが、複数のプロセスやスレッドなどから同じファイルを操作しようとすると思わぬことが発生することがあるので注意が必要です。
僕がこれまでに携わったプロジェクトでも以下のようなことがありました。
- 複数のプロセスから同一のログファイルに書き込んだらファイルが壊れた
- 参照しようとしたら別の処理が先に消していた
- あるサーバが作成したファイルを別のサーバから参照しようとしたら、存在するはずのファイルが見えない状態だった
これらは、設計ミスやOSのファイル管理方法を理解出来ていないことが原因で発生したものでした。
ファイルはOSと密接に関わっているため、ローカル環境では上手く動いても、別の環境では意図通りに動かないといったことがあります。
ファイルを扱う際は、設計時点で問題ないかをきちんと議論することをおすすめします。
今回はJavaのファイル操作について解説しました。
以上、参考になれば幸いです。
コメント