Posted at

java.io.File のコードを java.nio.Path と java.nio.Files を使って書き直す

More than 1 year has passed since last update.


概要

先日、勢いで java.io.File を使っていたコードを Path と Files で置き換えたので、既存のコードを書き換える、という観点で紹介します。すでに NIO2 をバリバリ使いこなしている方は、この記事を読んでも何の気付きもないと思います。

ちなみに JDK7 からの新しいファイル関連 API は NIO2(New I/O 2)と呼ぶそうで、無印は1.4の時代に追加された Channel や Charset のことを指すらしいです。


背景

File クラスには下記2つの機能があります。


  • ファイルの置き場所を定義する

  • ファイルを操作する

それを JDK7 の NIO2 では下記の通り分割したそうです。

今後 File クラスが Path & Files に完全に置き換わっていくのかどうか私は知りません。File クラスの Javadoc を見ると、1.7 で追加されたメソッドは toPath() だけで、 1.8 ではまったく新規メソッドがなかったようですので、そのことからすると、今後のファイル関連APIの開発は Files と Path にシフトしていくのではないかと思わなくもないです。ただ、 JDK では Path ではなく File のみを扱っているライブラリが今も結構あります。例として、 JavaFX の FileChooser や Drag & Drop API は File しか扱えませんので、 Files クラスで操作したい場合は File クラスの toPath() メソッドで Path オブジェクトに変換する必要があります。

いま java.io.File で動いているコードがあって、テストコードも万全であるなら、Java の後方互換性の歴史を考えても、無理して Path に書き換えていくこともないでしょう。事情があって JDK6 以前の環境で開発せざるを得ない場合もそうです。ただ、Files クラスの機能は単純に優秀なものが多いので、例えば新規で開発する部分には試しに NIO2 で書いてみる、というのは選択肢としてありだと思います。


注意

NIO2 は JDK7 から追加されています。おおよそ JDK7 のライブラリが揃っていると言われている Android SDK では、残念ながら java.nio パッケージのほとんどのクラスが実装されていません。

https://developer.android.com/reference/java/nio/package-summary.html

詳細は下記の記事をご覧ください。


この記事で書かないこと

すでに登場から5年以上が過ぎているライブラリでして、 Path や Files の詳細な使い方、 File からの置き換えという観点からは外れる部分については、ほかに優れた記事が多数ありますので、この記事では省略します。



オブジェクトの生成


File の場合

File はコンストラクタにファイルパスを文字列で(あるいは URL で)渡すことで生成するのが主だと思います。

File file = new File("dir/file.txt");


Path の場合

Paths というファクトリがあり、それの get() メソッドの引数に文字列ないし URI を渡すことでオブジェクトを生成します。

Path path = Paths.get("dir/file.txt");



File と Path の相互変換

File と Path は相互に変換することが可能です。前述の FileChooser のように、 Path を返さないライブラリで NIO を使いたい場合はこのメソッドで Path オブジェクトに変換するとよいです。


File -> Path

Path path = file.toPath();


Path -> File

File file = path.toFile();



ファイルの絶対パスを取得する


File の場合

絶対パスの文字列を返す getAbsolutePath を使います。

file.getAbsolutePath();


NIO2 の場合

toAbsolutePath で絶対パスの Path オブジェクトに変換してから、 toString() で文字列に変換します。

path.toAbsolutePath().toString(),



ファイルの存在を確認する


File の場合

file.exists();


NIO2 の場合

Files.exists(path);



ファイルが読み込み可能かを判定する


File の場合

File.canRead を使います。

file.canRead())


NIO2 の場合

Files#isReadable を使います。

Files.isReadable(Path)



フォルダを作成する


File の場合

File.mkdirs を使うと親フォルダも併せて作成してくれます。成否が boolean で返されます。

new File("dir").mkdirs();


NIO2 の場合

Files#createDirectories を使います。このメソッドは IOException が発生しうるため、例外を処理する必要があります。

Files.createDirectories(dirPath);



フォルダかを判定する

そんなに変わりません。


File の場合

file.isDirectory()


NIO2 の場合

Files.isDirectory(path)



ファイルを移動する


File の場合

File.renameTo を使います。成否が boolean で返ります。

file.renameTo(dest);


NIO2 の場合

Files#move を使います。変更後のファイルの Path が返ってきます。

Files.move(path, destPath);



ファイルを削除する


File の場合

new File("file.txt").delete();


NIO2 の場合

Files.delete(Paths.get("file.txt"));

なお、ファイルが存在する時に削除する deleteIfExists というメソッドもあり、こちらは boolean の値を返します。

Files.deleteIfExists(Paths.get("file.txt"));



フォルダ内の全ファイルを操作

実はこれが妙に難しかったりします。


java.io.File の場合

例えば、 dir というフォルダにある全ファイルを操作したい場合、java.io.File ではオブジェクトを配列で取得するコードを下記の通り書けました。

File[] files = new File("dir").listFiles();


NIO2 の場合

JDK8 なら Files#list(Path) を使えば簡単です。

Stream<Path> files = Files.list(Paths.get(articleDir));

Stream でなく List が欲しい場合は collect しましょう。

List<Path> files = Files.list(f).collect(Collectors.toList());

JDK7 の場合は Files.newDirectoryStream を使うことになるでしょうか……

try (final DirectoryStream<Path> directoryStream

= Files.newDirectoryStream(dir, Articles::isValidContentPath)){
// ……
} catch (final IOException e) {
e.printStackTrace();
}



ファイルの最終更新日時をミリ秒で取得


File の場合

lastModified() メソッドでミリ秒を取得できます。

final long lastModifiedMs = file.lastModified();


NIO2 の場合

Files.getLastModifiedTimeFileTime オブジェクトを取得できますので、それを toMillis() すれば、従来と同じように最終更新日時をミリ秒で取得できます。 FileTime クラスは別の変換メソッドがいろいろ用意されていますので、「秒で取得していたつもりがミリ秒だった」という Java あるあるをコーディングの段階で回避することが容易になりました。

Files.getLastModifiedTime(path).toMillis());

ただし、File#lastModified と違い、 getLastModifiedTime は IOException を発生させうるため、例外の処理が必要です。



Files のちょっと便利なメソッド

ついでにいくつか紹介します。


Files#newBufferedReader(Path path)

これまでだと下記のように Reader をラップしまくって初期化する必要があり、オレオレ FileUtils で FileReader を初期化して返すメソッドを作ったり Apache Commons を使ったりしたのではないでしょうか。

new BufferedReader(new InputStreamReader(new FileInputStream(pTargetFileName), pEncode));

Files & Path の場合、読み込むファイルのエンコーディングが UTF-8 なら下記でよいです。

final BufferedReader fileReader = Files.newBufferedReader(path)

第2引数で Charset を渡すことにより、別のエンコーディングのファイルも扱うことができます。


Files#newBufferedWriter

もちろん Writer のメソッドもあります。

final BufferedWriter writer = Files.newBufferedWriter(path);


Files#readAllLines

ファイルの中身を読み込んで1行ずつ List に入れたものを取得できます。この手のメソッドも自作して使い回していたのでありがたいです。IOException の例外処理が必要です。

final List<String> lines = Files.readAllLines(path);


Files#readAllBytes

小さいファイルを1つのbyte配列で読み込みたい時に使うメソッドです。

final byte[] bytes = Files.readAllBytes(path);


Files#write

byte[] の内容を、指定した Path のファイルに書き込みます。文字列 str の内容を path のファイルに出力する場合は下記の通りです。IOException の例外処理が必要です。

Files.write(path, str.getBytes(StandardCharsets.UTF_8.name()));



まとめ

既存の File のコードを Path と Files を使ったコードに置き換える方法を中心に述べました。例外処理の必要なメソッドが Files には多く、例外を適切に処理するプログラミングスタイルが必要になります。タイプする文字数も File の時に比べ、単純に増加する傾向がみられますので、最初はとっつきにくいところがあるかもしれないです。例外処理が必要なメソッドは Lambda 式との相性があまりよくないのが残念なところです。

それと、今回は書いていませんでしたが、Files クラスのメソッドは扱うファイルのエンコーディングが UTF-8 であることを前提としているものがほとんどなので、それ以外のエンコーディングを使っている場合は難しい対応を迫られるかもしれないです。



参考


書籍


Web