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

# 概要
先日、勢いで [java.io.File](https://docs.oracle.com/javase/jp/8/docs/api/java/io/File.html) を使っていたコードを Path と Files で置き換えたので、既存のコードを書き換える、という観点で紹介します。すでに NIO2 をバリバリ使いこなしている方は、この記事を読んでも何の気付きもないと思います。

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

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

- ファイルの置き場所を定義する
- ファイルを操作する

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

- ファイルの置き場所を定義する……[java.nio.file.Path](https://docs.oracle.com/javase/jp/8/docs/api/java/nio/file/Path.html) インターフェイス
- ファイルを操作する……[java.nio.file.Files](https://docs.oracle.com/javase/jp/8/docs/api/java/nio/file/Files.html)

今後 File クラスが Path & Files に完全に置き換わっていくのかどうか私は知りません。File クラスの Javadoc を見ると、1.7 で追加されたメソッドは toPath() だけで、 1.8 ではまったく新規メソッドがなかったようですので、そのことからすると、今後のファイル関連APIの開発は Files と Path にシフトしていくのではないかと思わなくもないです。ただ、 JDK では Path ではなく File のみを扱っているライブラリが今も結構あります。例として、 JavaFX の [FileChooser](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/stage/FileChooser.html) や 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

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

- [Androidはどの程度 Java7か?](http://qiita.com/dagezi/items/a16d661a60b9505ed853)

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

----
# オブジェクトの生成

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

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

## Path の場合
[Paths](https://docs.oracle.com/javase/jp/8/docs/api/java/nio/file/Paths.html) というファクトリがあり、それの ```get()``` メソッドの引数に文字列ないし URI を渡すことでオブジェクトを生成します。

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

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

## File -> Path

```java
Path path = file.toPath();
```

## Path -> File

```java
File file = path.toFile();
```

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

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

```java
file.getAbsolutePath();
```

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

```java
path.toAbsolutePath().toString(),
```

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

## File の場合

```java
file.exists();
```

## NIO2 の場合

```java
Files.exists(path);
```

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

## File の場合
```File.canRead``` を使います。

```java
file.canRead())
```

## NIO2 の場合
```Files#isReadable``` を使います。

```java
Files.isReadable(Path)
```

----
# フォルダを作成する

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

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

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

```java
Files.createDirectories(dirPath);
```

----
# フォルダかを判定する
そんなに変わりません。

## File の場合

```java
file.isDirectory()
```

## NIO2 の場合

```java
Files.isDirectory(path)
```

----
# ファイルを移動する

## File の場合
```File.renameTo``` を使います。成否が boolean で返ります。

```java
file.renameTo(dest);
```

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

```java
Files.move(path, destPath);
```

----
# ファイルを削除する

## File の場合

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

## NIO2 の場合

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

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

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

----
# フォルダ内の全ファイルを操作
実はこれが妙に難しかったりします。

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

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

## NIO2 の場合
JDK8 なら ```Files#list(Path)``` を使えば簡単です。

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

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

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

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

```java
try (final DirectoryStream<Path> directoryStream
                = Files.newDirectoryStream(dir, Articles::isValidContentPath)){
    // ……
} catch (final IOException e) {
    e.printStackTrace();
}
```

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

## File の場合
```lastModified()``` メソッドでミリ秒を取得できます。

```java
final long lastModifiedMs = file.lastModified();
```

## NIO2 の場合
```Files.getLastModifiedTime``` で [FileTime](https://docs.oracle.com/javase/jp/8/docs/api/java/nio/file/attribute/FileTime.html) オブジェクトを取得できますので、それを ```toMillis()``` すれば、従来と同じように最終更新日時をミリ秒で取得できます。 FileTime クラスは別の変換メソッドがいろいろ用意されていますので、「秒で取得していたつもりがミリ秒だった」という Java あるあるをコーディングの段階で回避することが容易になりました。

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

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

----
# Files のちょっと便利なメソッド
ついでにいくつか紹介します。

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

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

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

```java
final BufferedReader fileReader = Files.newBufferedReader(path)
```

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

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

```java
final BufferedWriter writer = Files.newBufferedWriter(path);
```

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

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

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


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

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

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

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

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

----
# 参考

## 書籍
- [『現場で使える［最新］Java SE 7/8 速攻入門』](http://gihyo.jp/book/2015/978-4-7741-7738-0)……NIO についての歴史や背景や意義、さらに使い方まで非常に詳しく紹介されています。

## Web
- [ファイル・ディレクトリ操作2](http://www.ne.jp/asahi/hishidama/home/tech/java/files.html)
- [参考メモ/Java7のnio2のファイル操作の便利機能(basename,dirname,realpath相当とファイルの簡易読み書き)](http://qiita.com/msakamoto_sf/items/e4ca526a8b5427029590)
- [NIOでフォルダを再帰処理する方法メモ](http://qiita.com/opengl-8080/items/7e75d194b22f0a54905f)