Javaのファイル入出力関係のクラス/インタフェースについて整理する

#はじめに
Javaのファイル入出力に関係するクラス/インタフェースはやたらと数が多い。
そこで、この記事ではファイル入出力に関係するクラス/インタフェースについて整理する。

今よく使われているものだけでなく、検索でよく引っかかるが古過ぎて今となってはもう使わないものについても説明する。
Javaはなまじ歴史の長い言語であるだけに古いクラス/インタフェースを使ったコードも検索でよく引っかかる。
そのとき古い書き方を古い書き方と認識できないと混乱するので、古いクラス/インタフェースについても知っておいた方が良いのである。

#大分類
まずは以下の4+2クラス/インタフェースとそれぞれの役割を知ろう。
この4+2クラス/インタフェースさえ押さえておけば、大筋で理解を誤ることは無い。

## 基本的な4クラス

<table>
<tr><th></th><th>バイト列</th><th>文字列</th></tr>
<tr><th>読み込み</th><td><code>java.io.InputStream</code></td><td><code>java.io.Reader</code></td></tr>
<tr><th>書き込み</th><td><code>java.io.OutputStream</code></td><td><code>java.io.Writer</code></td></tr>
</table>

読み込みのためのクラスと書き込みのためのクラスとがあり、また、バイト列を読み書きするためのクラスと文字列を読み書きするためのクラスとがあるので、全部で2×2で4クラスである。
バイト列を読み書きするためのクラスでは、`0xE3, 0x81, 0x82, 0xE3, 0x81, 0x84, 0xE3, 0x81, 0x86`のようなバイト列を読み書きすることができる。
文字列を読み書きするためのクラスでは、`"あいう"`のような文字列を読み書きすることができる。

## 補助的な2クラス/インタフェース

|ファイルパス|文字コード|
|---|---|
|`java.nio.file.Path`|`java.nio.charset.Charset`|

`Path`はファイルパスを表すインタフェースである。
あくまでファイルパスを表すだけであって、その場所にファイルが本当に存在するかどうかという情報は含まない。

`Charset`は文字コードを表すクラスである。
文字列をファイルに保存する際、ファイル上には文字列をそのまま文字列として保存することはできず、バイト列に変換して保存しなければならない。
この変換の方式を定めたものが文字コードである。
UTF-8という文字コードでは`"あいう"`という文字列は`0xE3, 0x81, 0x82, 0xE3, 0x81, 0x84, 0xE3, 0x81, 0x86`というバイト列と相互変換できる。
また、MS932という文字コードでは、`"あいう"`という文字列は`0x82, 0xA0, 0x82, 0xA2, 0x82, 0xA4`というバイト列と相互変換できる。

ファイルにバイト列を読み書きする場合は、`Path`の情報が必要になる。
ファイルに文字列を読み書きする場合は、`Path`の情報と`Charset`の情報が必要になる。

### `Path` vs `File`

昔は`java.nio.file.Path`の代わりに`java.io.File`というクラスが使われていた。
`Path`は`File`のAPIを使いやすく整理したり機能面での改良を施したりしたものである。
そのため、現在ではファイルパスを表すのには`File`ではなく`Path`を使うことが推奨されている。

古いAPIには`File`しか引数にとらないものもある。
その場合に備え、`Path`と`File`は相互変換が可能になっている。
`path.toFile()`で`Path`から`File`への変換が、`file.toPath()`で`File`から`Path`への変換ができるようになっている。

# 小分類
多くのクラスが基本4クラスを継承することによって作られている。
今はもう基本的に使われていないものには（古い）と説明に書いておいた。

<table><thead>
<tr><th colspan="3">継承関係</th><th>種別</th><th>説明</th></tr>
</thead><tbody>
<tr><td colspan="3"><code>Path</code></td><td>インタフェース</td><td>ファイルパス</td></tr>
<tr><td colspan="3"><code>File</code></td><td>具象クラス</td><td>ファイルパス（古い）</td></tr>
<tr><td colspan="3"><code>Charset</code></td><td>抽象クラス</td><td>文字コード</td></tr>
<tr><td colspan="3"><code>InputStream</code></td><td>抽象クラス</td><td>バイト列を読み込む</td></tr>
<tr><td></td><td colspan="2"><code>FileInputStream</code></td><td>具象クラス</td><td>ファイルからバイト列を読み込む（古い）</td></tr>
<tr><td colspan="3"><code>Reader</code></td><td>抽象クラス</td><td>文字列を読み込む</td></tr>
<tr><td></td><td colspan="2"><code>InputStreamReader</code></td><td>具象クラス</td><td>バイト列から変換して文字列を読み込む</td></tr>
<tr><td></td><td></td><td><code>FileReader</code></td><td>具象クラス</td><td>ファイルから文字列を読み込む（古い）</td></tr>
<tr><td></td><td colspan="2"><code>BufferedReader</code></td><td>具象クラス</td><td>バッファリングによって文字列を効率よく読み込む</td></tr>
<tr><td></td><td></td><td><code>LineNumberReader</code></td><td>具象クラス</td><td><code>BufferedReader</code>に行番号をカウントする機能を追加したもの</td></tr>
<tr><td colspan="3"><code>OutputStream</code></td><td>抽象クラス</td><td>バイト列を書き込む</td></tr>
<tr><td></td><td colspan="2"><code>FileOutputStream</code></td><td>具象クラス</td><td>ファイルにバイト列を書き込む（古い）</td></tr>
<tr><td></td><td colspan="2"><code>FilterOutputStream</code></td><td>具象クラス（ただしコンストラクタは<code>protected</code>）</td><td>バイト列を書き込む際何らかのフィルタ処理をする</td></tr>
<tr><td></td><td></td><td><code>PrintStream</code></td><td>具象クラス</td><td><code>OutputStream</code>を継承しているくせに何故か文字列も書き込める（古い）</td></tr>
<tr><td colspan="3"><code>Writer</code></td><td>抽象クラス</td><td>文字列を書き込む</td></tr>
<tr><td></td><td colspan="2"><code>OutputStreamWriter</code></td><td>具象クラス</td><td>バイト列に変換して文字列を書き込む</td></tr>
<tr><td></td><td></td><td><code>FileWriter</code></td><td>具象クラス</td><td>ファイルに文字列を書き込む（古い）</td></tr>
<tr><td></td><td colspan="2"><code>BufferedWriter</code></td><td>具象クラス</td><td>バッファリングによって文字列を効率的に書き込む</td></tr>
<tr><td></td><td colspan="2"><code>PrintWriter</code></td><td>具象クラス</td><td><code>Writer</code>に様々な機能を追加して使いやすくしたもの</td></tr>
</tbody></table>

## `Path`
- 完全修飾インタフェース名: `java.nio.file.Path`
- 継承関係: `Path`

ファイルパスを表すインタフェース。
`Paths.get("path/to/file")`のようにして使う。
`Paths.get("path/to/file").toFile()`のようにすることで`File`クラスへ変換可能。

## `File`
- 完全修飾クラス名: `java.io.File`
- 継承関係: `File`
- 古いAPIに対応するためにときどき使われる

ファイルパスを表すクラス。
`new File("path/to/file")`のようにして使う。
`File`しか引数にとらないような古いAPIに対応するために、未だに使われることがある。
`new File("path/to/file").toPath()`のようにすることで`Path`インタフェースへ変換可能。

## `Charset`
- 完全修飾クラス名: `java.nio.charset.Charset`
- 継承関係: `Charset`

文字コードを表すクラス。
`Charset.defaultCharset()`でデフォルトの文字コードを取得できる。
これは、Unix系OSでは基本的にUTF-8になり、Windowsでは基本的にMS932（Shift_JISのWindows方言）になる。
UTF-8を決め打ちで取得するには`StandardCharsets.UTF_8`とする。
MS932を決め打ちで取得するには`Charset.forName("MS932")`とする。

## `InputStream`
- 完全修飾クラス名: `java.io.InputStream`
- 継承関係: `InputStream`

バイト列を読み込むためのクラス。
ここから派生したクラスをいくつか持つ。

## `FileInputStream`
- 完全修飾クラス名: `java.io.FileInputStream`
- 継承関係: `InputStream` &gt; `FileInputStream`
- 古いので今となってはもう使われない

ファイルからバイト列を読み込むためのクラス。
以下のようにして使う。

```java
try (final InputStream is = new FileInputStream(new File("path/to/file"))) {
}
```

以下のようにすれば同じことができるため、今となってはもう使われない。

```java
try (final InputStream is = Files.newInputStream(Paths.get("path/to/file"))) {
}
```

## `Reader`
- 完全修飾クラス名: `java.io.Reader`
- 継承関係: `Reader`

文字列を読み込むためのクラス。
ここから派生したクラスをいくつか持つ。

## `InputStreamReader`
- 完全修飾クラス名: `java.io.InputStreamReader`
- 継承関係: `Reader` &gt; `InputStreamReader`

バイト列から変換して文字列を読み込むためのクラス。
以下のようにして使う。

```java
try (final InputStream is = Files.newInputStream(Paths.get("path/to/file"));
    final InputStreamReader isr = new InputStreamReader(is, StandardCharsets.UTF_8)) {
}
```

## `FileReader`
- 完全修飾クラス名: `java.io.FileReader`
- 継承関係: `Reader` &gt; `InputStreamReader` &gt; `FileReader`
- 古いので今となってはもう使われない

ファイルから文字列を読み込むためのクラス。
ファイルから文字列を読み込むための以下の記述が長いので省略できるように作られた。

```java
try (final InputStream is = new FileInputStream(new File("path/to/file"));
    final InputStreamReader isr = new InputStreamReader(is, Charset.defaultCharset())) {
}
```

以下のようにして使う。（デフォルト文字コードしか指定できない点に注意）

```java
try (final InputStreamReader isr = new FileReader(new File("path/to/file"))) {
}
```

以下のようにすればほぼ同じことができるため、今となってはもう使われない。

```java
try (final BufferedReader br = Files.newBufferedReader(Paths.get("path/to/file"), Charset.defaultCharset())) {
}
```

## `BufferedReader`
- 完全修飾クラス名: `java.io.BufferedReader`
- 継承関係: `Reader` &gt; `BufferedReader`

バッファリングと呼ばれる技術を用いて`Reader`から効率的に文字列を読み込むためのクラス。
以下のようにして使う。（下は上の省略表記）

```java
try (final InputStream is = Files.newInputStream(Paths.get("path/to/file"));
    final InputStreamReader isr = new InputStreamReader(is, StandardCharsets.UTF_8);
    final BufferedReader br = new BufferedReader(isr)) {
}
```

```java
try (final BufferedReader br = Files.newBufferedReader(Paths.get("path/to/file"), StandardCharsets.UTF_8)) {
}
```

## `LineNumberReader`
- 完全修飾クラス名: `java.io.LineNumberReader`
- 継承関係: `Reader` &gt; `BufferedReader` &gt; `LineNumberReader`

`BufferedReader`に行番号をカウントする機能を追加したクラス。
以下のようにして使う。

```java
try (final InputStream is = Files.newInputStream(Paths.get("path/to/file"));
    final InputStreamReader isr = new InputStreamReader(is, StandardCharsets.UTF_8);
    final LineNumberReader lnr = new LineNumberReader(isr)) {
}
```

行番号を表示する例は以下。

```java
/*
1: 1行目
2: 2行目
3: 3行目
…
のような出力がされる
*/
try (final InputStream is = Files.newInputStream(Paths.get("path/to/file"));
    final InputStreamReader isr = new InputStreamReader(is, StandardCharsets.UTF_8);
    final LineNumberReader lnr = new LineNumberReader(isr)) {
    String line;
    while ((line = lnr.readLine()) != null) {
        System.out.printf("%d: %s%n", lnr.getLineNumber(), line);
    }
}
```

## `OutputStream`
- 完全修飾クラス名: `java.io.OutputStream`
- 継承関係: `OutputStream`

バイト列を書き込むためのクラス。
ここから派生したクラスをいくつか持つ。

## `FileOutputStream`
- 完全修飾クラス名: `java.io.FileOutputStream`
- 継承関係: `OutputStream` &gt; `FileOutputStream`
- 古いので今となってはもう使われない

ファイルにバイト列を書き込むためのクラス。
以下のようにして使う。

```java
try (final OutputStream os = new FileOutputStream(new File("path/to/file"))) {
}
```

以下のようにすれば同じことができるため、今となってはもう使われない。

```java
try (final OutputStream os = Files.newOutputStream(Paths.get("path/to/file"))) {
}
```

## `FilterOutputStream`
- 完全修飾クラス名: `java.io.FilterOutputStream`
- 継承関係: `OutputStream` &gt; `FilterOutputStream`

バイト列を書き込む際何らかのフィルタ処理をするクラスはこのクラスを継承する。
普段はほぼ使わない。
後述の`PrintStream`の親クラスなので紹介した。

## `PrintStream`
- 完全修飾クラス名: `java.io.PrintStream`
- 継承関係: `OutputStream` &gt; `FilterOutputStream` &gt; `PrintStream`
- 古いので`System.out`と`System.err`以外はもう使われない

`OutputStream`を継承しているくせに何故か文字列も書き込めるクラス。
一応バイト列も書き込める。
おそらく入出力関係のAPIがきちんと設計される前に作られたクラスなのだと思われる。
そのような古いクラスであるため、このクラスのインスタンスを開発者が新たに作って使うことはもう無い。
しかし、`System.out`と`System.err`はこのクラスのインスタンスであるため、それらを扱うときだけはこのクラスを使うことになる。
`System.out`と`System.err`に文字列を書き込むと、デフォルト文字コードで変換されたバイト列がストリームに書き込まれる。

## `Writer`
- 完全修飾クラス名: `java.io.Writer`
- 継承関係: `Writer`

文字列を書き込むためのクラス。
ここから派生したクラスをいくつか持つ。

## `OutputStreamWriter`
- 完全修飾クラス名: `java.io.OutputStreamWriter`
- 継承関係: `Writer` &gt; `OutputStreamWriter`

バイト列に変換して文字列を書き込むためのクラス。
以下のようにして使う。

```java
try (final OutputStream os = Files.newOutputStream(Paths.get("path/to/file"));
    final OutputStreamWriter osw = new OutputStreamWriter(os, StandardCharsets.UTF_8)) {
}
```

## `FileWriter`
- 完全修飾クラス名: `java.io.FileWriter`
- 継承関係: `Writer` &gt; `OutputStreamWriter` &gt; `FileWriter`
- 古いので今となってはもう使われない

ファイルに文字列を書き込むためのクラス。
ファイルに文字列を書き込むための以下の記述が長いので省略できるように作られた。

```java
try (final OutputStream os = new FileOutputStream(new File("path/to/file"));
    final OutputStreamWriter osw = new OutputStreamWriter(osw, Charset.defaultCharset())) {
}
```

以下のようにして使う。（デフォルト文字コードしか指定できない点に注意）

```java
try (final OutputStreamWriter osw = new FileWriter(new File("path/to/file"))) {
}
```

以下のようにすればほぼ同じことができるため、今となってはもう使われない。

```java
try (final BufferedWriter bw = Files.newBufferedWriter(Paths.get("path/to/file"), Charset.defaultCharset())) {
}
```

## `BufferedWriter`
- 完全修飾クラス名: `java.io.BufferedWriter`
- 継承関係: `Writer` &gt; `BufferedWriter`

バッファリングと呼ばれる技術を用いて`Writer`に効率的に文字列を書き込むためのクラス。
以下のようにして使う。（下は上の省略表記）

```java
try (final OutputStream os = Files.newOutputStream(Paths.get("path/to/file"));
    final OutputStreamWriter osw = new OutputStreamWriter(os, StandardCharsets.UTF_8);
    final BufferedWriter bw = new BufferedWriter(osw)) {
}
```

```java
try (final BufferedWriter bw = Files.newBufferedWriter(Paths.get("path/to/file"), StandardCharsets.UTF_8)) {
}
```

## `PrintWriter`
- 完全修飾クラス名: `java.io.PrintWriter`
- 継承関係: `Writer` &gt; `PrintWriter`

`Writer`に`println`や`printf`などの便利なメソッドを追加して使いやすくしたクラス。
通常は`BufferedWriter`と組み合わせて以下のようにして使う。
`PrintWriter`の第二引数は`println`メソッド、`printf`メソッド、`format`メソッドの実行後に第一引数の`Writer`をflushするか否か。

```java
try (final BufferedWriter bw = Files.newBufferedWriter(Paths.get("path/to/file"), StandardCharsets.UTF_8);
    final PrintWriter pw = new PrintWriter(bw, false)) {
}
```

上の表記は以下のような省略表記が可能。

```java
try (final PrintWriter pw = new PrintWriter(new File("path/to/file"), StandardCharsets.UTF_8)) {
}
```

#コンストラクタ/ファクトリメソッドのまとめ
上で説明を省いたコンストラクタ/ファクトリメソッドも一部ある。
似たコンストラクタ/ファクトリメソッドを一まとめにするため、他言語におけるデフォルト引数のような書き方をしているものが一部ある。

<table><thead>
<tr><th colspan="3">継承関係</th><th>コンストラクタ/ファクトリメソッド</th><th>備考</th></tr>
</thead><tbody>
<tr><td colspan="3"><code>Path</code></td><td><code>Paths.get(String filePath)</code></td><td></td></tr>
<tr><td colspan="3"><code>File</code></td><td><code>new File(String filePath)</code></td><td></td></tr>
<tr><td rowspan="3" colspan="3"><code>Charset</code></td><td><code>Charset.defaultCharset()</code></td><td>デフォルト文字コードを取得する</td></tr>
<tr><td><code>StandardCharsets.UTF_8</code></td><td>UTF-8</td></tr>
<tr><td><code>Charset.forName("MS932")</code></td><td>MS932</td></tr>
<tr><td colspan="3"><code>InputStream</code></td><td><code>Files.newInputStream(Path filePath, OpenOption... options = new OpenOption[]{StandardOpenOption.READ})</code></td><td></td></tr>
<tr><td rowspan="2"></td><td rowspan="2" colspan="2"><code>FileInputStream</code></td><td><code>new FileInputStream(String filePath)</code></td><td rowspan="2"></td></tr>
<tr><td><code>new FileInputStream(File filePath)</code></td></tr>
<tr><td colspan="3"><code>Reader</code></td><td>（特になし）</td><td></td></tr>
<tr><td rowspan="2"></td><td rowspan="2" colspan="2"><code>InputStreamReader</code></td><td><code>new InputStreamReader(InputStream stream, String charsetName)</code></td><td rowspan="2"></td></tr>
<tr><td><code>new InputStreamReader(InputStream stream, Charset charset = Charset.defaultCharset())</code></td></tr>
<tr><td rowspan="2"></td><td rowspan="2"></td><td rowspan="2"><code>FileReader</code></td><td><code>new FileReader(String filePath)</code></td><td rowspan="2"><code>new InputStreamReader(new FileInputStream(filePath, Charset.defaultCharset()))</code>と等価</td></tr>
<tr><td><code>new FileReader(File filePath)</code></td></tr>
<tr><td rowspan="2"></td><td rowspan="2" colspan="2"><code>BufferedReader</code></td><td><code>new BufferedReader(Reader reader, int bufferSize = 8192)</code></td><td></td></tr>
<tr><td><code>Files.newBufferedReader(Path filePath, Charset charset = StandardCharsets.UTF_8)</code></td><td><code>new BufferedReader(new InputStreamReader(Files.newInputStream(filePath), charset), 8192)</code>と等価</td></tr>
<tr><td></td><td></td><td><code>LineNumberReader</code></td><td><code>new LineNumberReader(Reader reader, int bufferSize = 8192)</code></td><td></td></tr>
<tr><td colspan="3"><code>OutputStream</code></td><td><code>Files.newOutputStream(Path filePath, OpenOption... options = new OpenOption[]{StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE})</code></td><td></td></tr>
<tr><td rowspan="2"></td><td rowspan="2" colspan="2"><code>FileOutputStream</code></td><td><code>new FileOutputStream(String filePath, boolean append = false)</code></td><td rowspan="2"></td></tr>
<tr><td><code>new FileOutputStream(File filePath, boolean append = false)</code></td></tr>
<tr><td></td><td colspan="2"><code>FilterOutputStream</code></td><td><code>new FilterOutputStream(OutputStream stream)</code></td><td></td></tr>
<tr><td rowspan="3"></td><td rowspan="3"></td><td rowspan="3"><code>PrintStream</code></td><td><code>new PrintStream(OutputStream stream, boolean autoFlush = false, String charsetName = Charset.defaultCharset().name())</code></td><td></td></tr>
<tr><td><code>new PrintStream(String filePath, String charsetName = Charset.defaultCharset().name())</code></td><td rowspan="2"><code>new PrintStream(new FileOutputStream(filePath, false), false, charsetName);</code>と等価</td></tr><tr><td><code>new PrintStream(File filePath, String charsetName = Charset.defaultCharset().name())</code></td></tr>
<tr><td colspan="3"><code>Writer</code></td><td>（特になし）</td><td></td></tr>
<tr><td rowspan="2"></td><td rowspan="2" colspan="2"><code>OutputStreamWriter</code></td><td><code>new OutputStreamWriter(OutputStream stream, String charsetName)</code></td><td rowspan="2"></td></tr>
<tr><td><code>new OutputStreamWriter(OutputStream stream, Charset charset = Charset.defaultCharset())</code></td></tr>
<tr><td rowspan="2"></td><td rowspan="2"></td><td rowspan="2"><code>FileWriter</code></td><td><code>new FileWriter(String filePath, boolean append = false)</code></td><td rowspan="2"><code>new OutputStreamWriter(new FileOutputStream(filePath, append), Charset.defaultCharset());</code>と等価</td></tr>
<tr><td><code>new FileWriter(File filePath, boolean append = false)</code></td></tr>
<tr><td rowspan="2"></td><td rowspan="2" colspan="2"><code>BufferedWriter</code></td><td><code>new BufferedWriter(Writer writer, int bufferSize = 8192)</code></td><td></td></tr>
<tr><td><code>Files.newBufferedWriter(Path filePath, Charset charset = StandardCharsets.UTF_8, OpenOption... options = new OpenOption[]{StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE})</code></td><td><code>new BufferedWriter(new OutputStreamWriter(Files.newOutputStream(filePath, options), charset), 8192)</code>と等価</td></tr>
<tr><td rowspan="4"></td><td rowspan="4" colspan="2"><code>PrintWriter</code></td><td><code>new PrintWriter(Writer writer, boolean autoFlush = false)</code></td><td></td></tr>
<tr><td><code>new PrintWriter(OutputStream stream, boolean autoFlush = false)</code></td><td><code>new PrintWriter(new BufferedWriter(new OutputStreamWriter(stream, Charset.defaultCharset()), 8192), autoFlush)</code>と等価</td></tr>
<tr><td><code>new PrintWriter(String filePath, String charsetName = Charset.standardCharset().name())</code></td><td rowspan="2"><code>new PrintWriter(new BufferedWriter(new OutputStreamWriter(new FileOutputStream(filePath, false), charsetName), 8192), false)</code>と等価</td></tr>
<tr><td><code>new PrintWriter(File filePath, String charsetName = Charset.standardCharset().name())</code></td></tr>
</tbody></table>

# 結局
結局のところ、今は下の3つの書き方だけ覚えていれば基本的なケースには対応できるんじゃないかと思われる。

```java
try (final Stream<String> lines = Files.lines(Paths.get("path/to/file"), StandardCharsets.UTF_8)) {
    lines.forEachOrdered(line -> {
        // 入力処理
    });
} catch (final IOException e) {
    // 例外処理
}

try (final BufferedWriter bw = Files.newBufferedWriter(Paths.get("path/to/file"), StandardCharsets.UTF_8);
    final PrintWriter pw = new PrintWriter(bw, true)) {
    pw.println("出力処理（新規書き込み）");
} catch (final IOException e) {
    // 例外処理
}

try (final BufferedWriter bw = Files.newBufferedWriter(Paths.get("path/to/file"), StandardCharsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.APPEND);
    final PrintWriter pw = new PrintWriter(bw, true)) {
    pw.println("出力処理（追加書き込み）");
} catch (final IOException e) {
    // 例外処理
}
```

ただ、これらのコードの内部で使われている多くのクラス、そしてこれらのコードに至るまでに作られてきた多くの古いクラスについて知っておいた方が少し凝ったことをしようとしたときに応用が利くはずである。
この記事がそうした知識の整理に少しでも役に立てば幸いである。