やり直しJava

ファイルとディレクトリ操作

Java SE7以前では ファイル操作を行うために java.io.Fileクラスが用意されていました。しかし Fileクラスには次のような欠点がありました。

  • 多くのメソッドで 失敗時に例外がスローされず 有用なエラーメッセージを取得できません。例えば ファイル削除に失敗した場合、失敗したことはわかるけれども 詳細な理由を知ることができません。
  • rename()メソッドの動作が プラットフォーム間で一貫していません。
  • シンボリックリンクが十分にサポートされていません。
  • ファイル権限・ファイル所有者・その他セキュリティ属性などの メタデータに対するサポートが不十分です。

これらの欠点を補い ファイル操作の利便性を向上させるために、Java SE 7からファイルとディレクトリ操作のためのパッケージ java.nio.fileが追加されました。java.io.Fileでは Fileがファイルやディレクトリを表し ファイル操作も全てFileクラスに集約されていましたが、java.nio.fileでは Pathがファイルやディレクトリを表し、ファイル操作は主にFilesクラスに集約され、役割が分担されるようになりました。

旧来のコードからの移行を促進するために Fileからjava.nio.file.Pathに変換するtoPath()メソッドが Fileクラスに追加されました。逆に、java.nio.fileに移行していないライブラリの使用に支障がないように、java.nio.file.Pathには Fileに変換するtoFile()メソッドも備えられていて、相互変換が行えるようになっています

旧来のファイルI/Oコード」(Javaチュートリアル:Oracle)

ファイルやディレクトリを表すPath

Java SE 7の前までは ファイルやディレクトリを表すのにjava.io.Fileクラスを用いていましたが、java.nio.fileでは Pathインタフェースがその役割を果たします。Pathはインタフェースであり、Pathを実装したオブジェクトを得るには次のように行います。

FileSystem fs = FileSystems.getDefault();
Path path1 =	fs.getPath("C:/work/sample.txt");
Path path2 =	fs.getPath("C:", "work", "sample.txt");

// Paths のクラスメソッド get でも同じように取得できる
Path path3 =	Paths.get("C:/work/sample.txt");
Path path4 =	Paths.get("C:", "work", "sample.txt");

FileオブジェクトからPathを取得することも、その逆もできます。

Path path = new File("C:/work/sample.txt").toPath();
File file = path.toFile();

Pathを実装したオブジェクトは、Fileと同様 不変オブジェクトになります

ファイルやディレクトリ操作を行うFiles

java.nio.files.Filesには、ファイル入出力や ファイル・ディレクトリ操作のためのさまざまなクラスメソッドが用意されています。

ファイル入出力関連

ファイル入出力関連のクラスメソッドを次にまとめます。

Filesクラスの主なファイル入出力メソッド
ファイル種別操作メソッド概要
テキストファイル読み込みreadAllLinesファイル全体を読み込み、各行のリスト(List)を返します。
linesファイル全体を読み込み、各行のストリーム(Stream)を返します。
newBufferedReaderファイルを読み込むためのBufferedReaderを作成して返します。
書き出しwrite各行のリスト(List)をまとめてファイルに書き出します。
newBufferedWriterファイルへ書き出すためのBufferedWriterを作成して返します。
バイナリファイル読み込みreadAllBytesファイル全体を読み込み、バイト配列(byte[])として返します。
newInputStreamファイルを読み込むためのInputStreamを作成して返します。
書き出しwriteバイト配列(byte[])をまとめてファイルに書き出します。
newOutputStreamファイルへ書き出すためのOutputStreamを作成して返します。

テキストファイルの入出力の各メソッドでは 文字コードを指定することができます。デフォルトはUTF-8です。また、書き出し用のメソッドでは ファイルオープン時のオプション(上書き、追記、新規作成、同期など)を指定することができます。

ファイル・ディレクトリ操作関連

ファイル・ディレクトリ操作関連のクラスメソッドを次にまとめます。

Filesクラスの主なファイル・ディレクトリ操作メソッド
操作メソッド概要
コピーcopyコピー元とコピー先をPathで指定して ファイルをコピーします。コピー元にInputStreamを指定するメソッドと、コピー先にOutputStreamを指定するメソッドがオーバーロードされています。
移動move移動元と移動先のPathを指定して ファイルを移動します。
ファイル新規作成createFile空のファイルを作成します。ファイルが既に存在している場合はFileAlreadyExistsExceptionが発生します。
ディレクトリ新規作成createDirectoryディレクトリを作成します。親ディレクトリが存在しない場合はNoSuchFileExceptionが発生します。
createDirectoriesディレクトリを作成します。親ディレクトリが存在しない場合は作成します。
ハードリンク作成createLinkハードリンクを作成します。(WindowsでもNTFSであればハードリンクを作成できます。)
シンボリックリンク作成createSymbolicLinkシンボリックリンクを作成します。(Windowsの場合はショートカットが作成されます。管理者権限で実行する必要があります。)
削除deleteファイルやディレクトリを削除します。ファイルが存在しない場合はNoSuchFileExceptionが発生します。また、ディレクトリの場合、空でないとDirectoryNotEmptyExceptionが発生します。
deleteIfExistsファイルやディレクトリが存在する場合だけ削除します。ディレクトリの場合、空でないとDirectoryNotEmptyExceptionが発生します。
一覧取得list指定したディレクトリ直下のファイル・ディレクトリの一覧をストリーム(Stream)の形で返します。Streamの形で返されるので、filter()で条件を指定して絞込みをしたり、forEach()で対象のファイル・ディレクトリに対して処理を行うことができます。
walk指定したディレクトリの配下(サブレディレクトリを含む)のファイル・ディレクトリの一覧をストリーム(Stream<Path>)の形で返します。上のlist()と同様に filter()で条件を指定して絞込みをしたり、forEach()で対象のファイル・ディレクトリに対して処理を行うことができます。
取得するサブディレクトリの最大階層を指定することもできます。

パス名の正規化

ユーザがファイル名を指定して、指定されたファイルを開くような場合には、ディレクトリトラバーサルの脆弱性を突かれないように パス名の検証が必要です。パス名を検証するにあたって、カレントディレクトリを表す「.」や親ディレクトリを表す「..」、シンボリックリンク等が含まれていると 検証が困難になります。そのような場合に パス名の正規化をすることで、パス名の検証が容易になります。

java.io.Fileの場合は、getCanonicalFile()やgetCanonicalPath()を呼び出すことによって 正規化したパス名を取得することができました。Pathには getCanonicalFile()やgetCanonicalPath()に相当するメソッドがありません。代わりに 次のようなメソッドで 正規化されたパス名を取得することができます。

  1. toAbsolutePath().nomalize() または normalize().toAbsolutePath()
  2. toRealPath()

1番目は normalize()だけでは 相対パスが相対パスのままになってしまうため、toAbsolutePath()とセットで呼び出します。

2番目の toRealPath()は 可変長引数になっていて、引数なしの場合は Pathがシンボリックリンクであれば リンク先のターゲットに解決されます。引数に LinkOption.NOFOLLOW_LINKS を指定すると、シンボリックリンクを解決しないように 挙動を制御することができます。また、toRealPath()は ファイルの存在確認を行い、存在しないファイルを指定した場合は例外が投げられます。そのため、新規ファイルを作成するような場合には 使うことができないことに注意が必要です。

File.getCanonicalPath()、Path.toAbsolutePath().normalize()、Path.toRealPath()などは 動作に細かな違いがあります。stackoverflowに 細かな違いがまとめられていましたので、興味があれば確認してみてください。

Difference between getCanonicalPath and toRealPath」(stackoverflow)