Javaでよくやるファイル操作（NIO.2編）

# 環境
JDK 17

# 主要なクラス・インタフェース
ファイル操作に関するクラス・インタフェースの多くは[`java.nio.file`パッケージ](https://docs.oracle.com/javase/jp/17/docs/api/java.base/java/nio/file/package-summary.html)に属しています。

これらのクラス・インタフェースを総称して「NIO.2」と呼ばれます。

## `Path`インタフェース
[`Path`インタフェース](https://docs.oracle.com/javase/jp/17/docs/api/java.base/java/nio/file/Path.html)は、名前の通りファイルやディレクトリのパスを表します。`Path.of("ファイルやディレクトリのパス")`で`Path`オブジェクトを生成できます。

```java
Path targetFile = Path.of("/tmp/test/sample.txt");
```

## `Files`クラス
ファイルまたはディレクトリに関する操作（読み込みや書き込みなど）は[`Files`クラス](https://docs.oracle.com/javase/jp/17/docs/api/java.base/java/nio/file/Files.html)のstaticメソッドとしてまとまっています。

いろんなメソッドが用意されているので、ぜひ[Javadoc](https://docs.oracle.com/javase/jp/17/docs/api/java.base/java/nio/file/Files.html)を一通り眺めてみてください！

## 文字コード
文字コードは[`Charset`抽象クラス](https://docs.oracle.com/javase/jp/17/docs/api/java.base/java/nio/charset/Charset.html)が表します。`Charset.forName("文字コード名")`で、指定した文字コードを表す`Charset`オブジェクトを生成できます。

```java:MS932（Windowsデフォルトの文字コード）を取得する例
Charset cs = Charset.forName("MS932");
```

```java:EUC-JPを取得する例
Charset cs = Charset.forName("EUC-JP");
```

> 文字コード名が正しくない場合は`UnsupportedCharsetException`がスローされます。

UTF-8やUTF-16などを指定する場合は[`StandardCharsets`クラス](https://docs.oracle.com/javase/jp/17/docs/api/java.base/java/nio/charset/StandardCharsets.html)の定数を利用できます。

```java
Charset cs = StandardCharsets.UTF_8;
```

```java
Charset cs = StandardCharsets.UTF_16;
```

# テキストファイルの読み込み
```text:/tmp/test/sample.txt（読み込み対象）
あいうえお
かきくけこ
さしすせそ
```

## 1行ずつ読み込んで加工する
[`Files.lines()`メソッド](https://docs.oracle.com/javase/jp/17/docs/api/java.base/java/nio/file/Files.html#lines(java.nio.file.Path,java.nio.charset.Charset))を利用すると、各行を要素に持つ`Stream<String>`を取得できます。

今回は各行を`[]`で囲うように変換します。

```java
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.stream.Stream;

public class Main {
    public static void main(String[] args) {
        Path targetFile = Path.of("/tmp/test/sample.txt");
        // 処理終了後に自動的にStreamがclose()されるようにtry-with-resourcesを利用
        try (Stream<String> stream = Files.lines(targetFile, StandardCharsets.UTF_8)) {
            // 各行を[]で囲う
            List<String> list = stream.map(line -> "[" + line + "]")
                    .toList();
            for (String line : list) {
                // 読み込んだ行に対して何らかの処理を行う
                // 今回はサンプルなので表示するだけ
                System.out.println(line);
            }
        } catch (IOException e) {
            // 業務コードでe.printStackTrace()を使うことはめったにありません。
            // 業務では各プロジェクトのルールに合わせて、例外の再スローやログの出力などを行ってください。
            e.printStackTrace();
        }
    }
}
```

> 引数が`Path`のみで文字コードを指定しない[`lines()`](https://docs.oracle.com/javase/jp/17/docs/api/java.base/java/nio/file/Files.html#lines(java.nio.file.Path))メソッドもあります。このメソッドでは、文字コードはUTF-8になります。

```text:実行結果
[あいうえお]
[かきくけこ]
[さしすせそ]
```

## BufferedReaderを使う
ほとんどの場合は`Files.lines()`メソッドで事足りると思います。

しかし、どうしても従来の`BufferedReader`を使いたい場合は、[`Files.newBufferedReader()`メソッド](https://docs.oracle.com/javase/jp/17/docs/api/java.base/java/nio/file/Files.html#newBufferedReader(java.nio.file.Path,java.nio.charset.Charset))で取得できます。

```java
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;

public class Main {
    public static void main(String[] args) {
        Path targetFile = Path.of("/tmp/test/sample.txt");
        // 処理終了後に自動的にBufferedReaderがclose()されるようにtry-with-resourcesを利用
        try (BufferedReader reader = Files.newBufferedReader(
                targetFile, StandardCharsets.UTF_8)) {
            // reader.readLine()で次の1行を読み込む（ファイルの末尾に到達した場合はnullを返す）
            for (String line; (line = reader.readLine()) != null;) {
                System.out.println(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
```

```shell:実行結果
あいうえお
かきくけこ
さしすせそ
```

## 全行を保持するListを生成する
[`Files.readAllLines()`](https://docs.oracle.com/javase/jp/17/docs/api/java.base/java/nio/file/Files.html#readAllLines(java.nio.file.Path,java.nio.charset.Charset))を利用します。

```java
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        Path targetFile = Path.of("/tmp/test/sample.txt");
        try {
            // 1要素＝1行であるListを生成する
            List<String> lines = Files.readAllLines(targetFile, StandardCharsets.UTF_8);
            for (String line : lines) {
                System.out.println(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
```

```shell:実行結果
あいうえお
かきくけこ
さしすせそ
```

> このメソッドはファイルの全内容を一気に読み込むので、ファイルが非常に大きい場合に`OutOfMemoryError`を引き起こす可能性があります。テキストファイルのサイズが実行環境のメモリ量より十分に小さい場合のみ、このメソッドを利用してください。

## ファイルの全内容を1つの文字列として取得する
[`Files.readString()`](https://docs.oracle.com/javase/jp/17/docs/api/java.base/java/nio/file/Files.html#readString(java.nio.file.Path,java.nio.charset.Charset))を利用します。

```java
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;

public class Main {
    public static void main(String[] args) {
        Path targetFile = Path.of("/tmp/test/sample.txt");
        try {
            String content = Files.readString(targetFile, StandardCharsets.UTF_8);
            System.out.println(content);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
```

```shell:実行結果
あいうえお
かきくけこ
さしすせそ
```

> このメソッドも、ファイルの全内容を一気に読み込むので注意してください。

# テキストファイルの書き込み
```text:/tmp/test/sample.txt（書き込み対象）
あいうえお
かきくけこ
さしすせそ
```

## StandardOpenOption
[`StandardOpenOption`列挙型](https://docs.oracle.com/javase/jp/17/docs/api/java.base/java/nio/file/StandardOpenOption.html)は、ファイルの開き方および作成方法を表します。

後述する`Files`クラスの各メソッドでは、最後の引数に`StandardOpenOption`を指定します（可変長引数なので複数指定可能）。

- `CREATE`
    - ファイルが存在しない場合は新規作成します。
    - ファイルが存在する場合は何もしません。
- `CREATE_NEW`
    - ファイルが存在しない場合は新規作成します。
    - ファイルが存在する場合は例外がスローされます。
- `APPEND`
    - 既存の内容の最後に追記します。
- `TRUNCATE_EXISTING`
    - 既存の内容を上書きします。

## 1行だけ書き込む（追記）
[`Files.writeString()`](https://docs.oracle.com/javase/jp/17/docs/api/java.base/java/nio/file/Files.html#writeString(java.nio.file.Path,java.lang.CharSequence,java.nio.charset.Charset,java.nio.file.OpenOption...))を利用します。

第4引数（可変長）に`StandardOpenOption.CREATE`（ファイルが存在する場合に例外にするなら`StandardOpenOption.CREATE_NEW`）・`StandardOpenOption.APPEND`を指定すると、ファイル末尾への追記ができます。

```java
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;

public class Main {
    public static void main(String[] args) {
        Path targetFile = Path.of("/tmp/test/sample.txt");
        try {
            Files.writeString(targetFile, "たちつてと", StandardCharsets.UTF_8,
                StandardOpenOption.CREATE,
                StandardOpenOption.APPEND);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
```

```text:実行後の/tmp/test/sample.txt
あいうえお
かきくけこ
さしすせそ
たちつてと
```

## 1行だけ書き込む（上書き）
[`Files.writeString()`](https://docs.oracle.com/javase/jp/17/docs/api/java.base/java/nio/file/Files.html#writeString(java.nio.file.Path,java.lang.CharSequence,java.nio.charset.Charset,java.nio.file.OpenOption...))メソッドを利用します。


第4引数（可変長）に`StandardOpenOption.CREATE`（ファイルが存在する場合に例外にするなら`StandardOpenOption.CREATE_NEW`）・`StandardOpenOption.TRUNCATE_EXISTING`を指定すると、上書きになります。つまり、以前のファイルの内容は削除されます。

```java
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;

public class Main {
    public static void main(String[] args) {
        Path targetFile = Path.of("/tmp/test/sample.txt");
        try {
            Files.writeString(targetFile, "たちつてと", StandardCharsets.UTF_8,
                StandardOpenOption.CREATE,
                StandardOpenOption.TRUNCATE_EXISTING);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
```

```text:実行後の/tmp/test/sample.txt
たちつてと
```

## 複数行を一気に書き込む（追記）
[`Files.write()`](https://docs.oracle.com/javase/jp/17/docs/api/java.base/java/nio/file/Files.html#write(java.nio.file.Path,java.lang.Iterable,java.nio.charset.Charset,java.nio.file.OpenOption...))を利用します。

第4引数（可変長）に`StandardOpenOption.CREATE`（ファイルが存在する場合に例外にするなら`StandardOpenOption.CREATE_NEW`）・`StandardOpenOption.APPEND`を指定すると、ファイル末尾への追記ができます。

```java
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        Path targetFile = Path.of("/tmp/test/sample.txt");
        try {
            List<String> lines = List.of("たちつてと", "なにぬねの", "はひふへほ");
            Files.write(targetFile, lines, StandardCharsets.UTF_8,
                StandardOpenOption.CREATE,
                StandardOpenOption.APPEND);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
```

```text:実行後の/tmp/test/sample.txt
あいうえお
かきくけこ
さしすせそ
たちつてと
なにぬねの
はひふへほ
```

## 複数行を一気に書き込む（上書き）
第4引数（可変長）に`StandardOpenOption.CREATE`（ファイルが存在する場合に例外にするなら`StandardOpenOption.CREATE_NEW`）・`StandardOpenOption.TRUNCATE_EXISTING`を指定すると、上書きになります。

```java
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        Path targetFile = Path.of("/tmp/test/sample.txt");
        try {
            List<String> lines = List.of("たちつてと", "なにぬねの", "はひふへほ");
            Files.write(targetFile, lines, StandardCharsets.UTF_8,
                StandardOpenOption.CREATE,
                StandardOpenOption.TRUNCATE_EXISTING);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
```

```text:実行後の/tmp/test/sample.txt
たちつてと
なにぬねの
はひふへほ
```

## 書き込み時の改行コードを明示的に指定
`writeString()`・`write()`では、改行コードは実行したOSのデフォルト（macOSとLinuxはLF、WindowsはCRLF）となります。

デフォルトでない改行コードを指定したい場合は、[`BufferedWriterクラスのwrite()メソッド`](https://docs.oracle.com/javase/jp/17/docs/api/java.base/java/io/Writer.html#write(java.lang.String))を利用します。

```java
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        Path targetFile = Path.of("/tmp/test/sample.txt");
        try (BufferedWriter bufferedWriter = Files.newBufferedWriter(targetFile,
                StandardCharsets.UTF_8,
                StandardOpenOption.CREATE,
                StandardOpenOption.TRUNCATE_EXISTING)) {
            List<String> lines = List.of("たちつてと", "なにぬねの", "はひふへほ");
            for (String line : lines) {
                bufferedWriter.write(line);
                bufferedWriter.write("\r\n");  // 改行コードにCRLFを指定（LFの場合は"\n"）
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
```

```text:実行後の/tmp/test/sample.txt
たちつてと
なにぬねの
はひふへほ
```

> `StandardOpenOption`は`CREATE_NEW`や`APPEND`でもOKです。