Javaでテキストファイルに書き込むなら、短い内容は Files.writeString()、何行も続けて書くなら Files.newBufferedWriter() を try-with-resources で開くのが、いま標準的な書き方です。 どちらも文字コードを省くと UTF-8 で書き込み、追記するかどうかは StandardOpenOption で指定します。FileWriter や BufferedWriter を自分で組み立てる従来の書き方も引き続き使えます。

この記事では、6つの書き方を同じ内容で動かして比べたうえで、追記の指定、StandardOpenOption の組み合わせごとの挙動、改行コードの違い、Shift_JIS での出力、close し忘れで中身が空になる例までを、すべて実行結果つきで紹介します。コードはそのまま貼り付けて java ファイル名.java で動く完成形です。

確認環境:Ubuntu 24.04 / OpenJDK 21.0.10(ロケールは C.UTF-8)。掲載している出力はすべてこの環境で実行したものです。

ファイル書き込みの早見表

6つの書き方を「追記・文字コード指定・改行・対応 JDK」で比べた表です。

書き方 追記の指定 文字コードの指定 指定しないときの文字コード 改行の入れ方 使える JDK
FileWriter コンストラクタの末尾に true 引数で可(JDK 11 以降) デフォルト文字セット(JDK 18 以降は UTF-8) \n を自分で書く 8 でも可
BufferedWriter(FileWriter を包む) 内側の FileWriter に従う 内側の FileWriter に従う 同上 newLine() 8 でも可
PrintWriter(BufferedWriter を包む) 内側に従う 内側に従う 同上 println()、printf の %n 8 でも可
Files.write(path, 行のリスト) StandardOpenOption.APPEND 引数で可 常に UTF-8 各行の後ろに自動で入る 8 でも可
Files.writeString(path, 文字列) StandardOpenOption.APPEND 引数で可 常に UTF-8 文字列に含めたものがそのまま出る 11 以降
Files.newBufferedWriter(path) StandardOpenOption.APPEND 引数で可 常に UTF-8 newLine() 8 でも可

「8 でも可」は、javac --release 8 でコンパイルが通ることを確かめたものです。

やりたいことから引く表も載せておきます。

やりたいこと 書き方
文字列を1回で書き込む(上書き) Files.writeString(path, text)
末尾に1行追記する Files.writeString(path, text, CREATE, APPEND)
何行もループで書く try (BufferedWriter bw = Files.newBufferedWriter(path)) { ... }
書式つきで書く(printf) new PrintWriter(Files.newBufferedWriter(path))
行のリストをまとめて書く Files.write(path, lines)
Shift_JIS で書く 第2引数に Charset.forName("Shift_JIS")(Windows 向けは "MS932")
すでにあるファイルを上書きしたくない StandardOpenOption.CREATE_NEW
途中の出力先フォルダも作る 先に Files.createDirectories(path.getParent())

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Java ファイル書き込み|BufferedWriterと追記・文字コード」

6つの書き方をコードと実行結果で比べる

FileWriter:いちばん基本の形

import java.io.FileWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

public class Write1 {
    public static void main(String[] args) throws IOException {
        try (FileWriter fw = new FileWriter("w1.txt", StandardCharsets.UTF_8)) {
            fw.write("備品A,10\n");
            fw.write("備品B,25\n");
        }
    }
}
$ java Write1.java
$ cat w1.txt
備品A,10
備品B,25

try ( ... ) { } は try-with-resources という構文で、ブロックを抜けるときに close() を自動で呼びます。以前は finally の中で手動で close() を呼ぶ書き方が一般的でしたが、この構文なら閉じ忘れが起きません。

第2引数に文字コードを渡せるコンストラクタは JDK 11 で追加されたものです。javac --release 8 でコンパイルすると次のエラーになります。

Rel2.java:7: error: no suitable constructor found for FileWriter(String,Charset)
        try (FileWriter fw = new FileWriter("rel2.txt", StandardCharsets.UTF_8)) {
                             ^

JDK 8 で文字コードを指定したいときは、new OutputStreamWriter(new FileOutputStream("w1.txt"), StandardCharsets.UTF_8) を使います(この形が --release 8 で通ることも確認しています)。

BufferedWriter:ためてからまとめて書く

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

public class Write2 {
    public static void main(String[] args) throws IOException {
        try (BufferedWriter bw = new BufferedWriter(
                new FileWriter("w2.txt", StandardCharsets.UTF_8))) {
            bw.write("備品A,10");
            bw.newLine();
            bw.write("備品B,25");
            bw.newLine();
        }
    }
}
$ java Write2.java
$ cat w2.txt
備品A,10
備品B,25

BufferedWriter は、書き込む文字をいったんメモリ(バッファ)にためて、ある程度たまったところでまとめてファイルへ送ります。公式の API ドキュメント(Java SE 21)も、FileWriter や OutputStreamWriter のように write() のコストが大きくなりうる Writer は BufferedWriter で包むことを勧めています。改行は newLine() で入れます。

PrintWriter:printlnとprintfが使える

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;

public class Write3 {
    public static void main(String[] args) throws IOException {
        try (PrintWriter pw = new PrintWriter(new BufferedWriter(
                new FileWriter("w3.txt", StandardCharsets.UTF_8)))) {
            pw.println("備品A,10");
            pw.printf("%s,%d%n", "備品B", 25);
            pw.println(3.5);
        }
    }
}
$ java Write3.java
$ cat w3.txt
備品A,10
備品B,25
3.5

System.out と同じ感覚で println() や printf() が使え、数値もそのまま渡せます。書式を整えた出力に向いています。ひとつ知っておきたいのは、PrintWriter のメソッドは入出力エラーが起きても例外を投げないことです。書き込めたかを確かめたいときは pw.checkError() の戻り値を見ます。

Files.write:行のリストをまとめて書く

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 Write4 {
    public static void main(String[] args) throws IOException {
        List<String> lines = List.of("備品A,10", "備品B,25");
        Files.write(Path.of("w4.txt"), lines, StandardCharsets.UTF_8);
    }
}
$ java Write4.java
$ cat w4.txt
備品A,10
備品B,25

行のリストを渡すと、各行の後ろに改行を付けて書き込みます。開く・書く・閉じるが1回の呼び出しで済むので、try-with-resources も不要です。

Files.writeString:文字列を1回で書く

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

public class Write5 {
    public static void main(String[] args) throws IOException {
        Files.writeString(Path.of("w5.txt"), "備品A,10\n備品B,25\n");
    }
}
$ java Write5.java
$ cat w5.txt
備品A,10
備品B,25

いちばん短く書ける方法です。JDK 11 で追加されたメソッドで、文字コードを省くと UTF-8 で書き込みます。--release 8 でコンパイルすると、次のとおり見つからないというエラーになります。

Rel.java:7: error: cannot find symbol
        Files.writeString(Paths.get("rel.txt"), "A\n");
             ^
  symbol:   method writeString(Path,String)
  location: class Files

Files.newBufferedWriter:何行も続けて書く

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

public class Write6 {
    public static void main(String[] args) throws IOException {
        Path path = Path.of("w6.txt");
        try (BufferedWriter bw = Files.newBufferedWriter(path, StandardCharsets.UTF_8)) {
            for (int i = 1; i <= 3; i++) {
                bw.write("行" + i);
                bw.newLine();
            }
        }
    }
}
$ java Write6.java
$ cat w6.txt
行1
行2
行3

BufferedWriter を1行で用意できます。ループで少しずつ書く処理や、大量の行を出力する処理に向いています。PrintWriter の機能も欲しいときは new PrintWriter(Files.newBufferedWriter(path)) と包みます。

どれを選ぶか

状況 選ぶ書き方 理由
短い文字列を1回書く Files.writeString 1行で済み、閉じ忘れがない
行のリストがすでにある Files.write 改行を自動で付けてくれる
ループで何行も書く、量が多い Files.newBufferedWriter 開いたまま効率よく書ける
書式つきで書きたい PrintWriter printf、println が使える
JDK 8 でも動かす Files.write / Files.newBufferedWriter 8 でも使え、常に UTF-8
既存コードが FileWriter そのまま、文字コードの引数を足す 変更が最小で済む

ファイルに追記する

何も指定しないと、どの書き方も上書き(中身を空にしてから書く)になります。末尾に足したいときの指定方法を、4通りまとめて動かします。

import java.io.BufferedWriter;
import java.io.FileWriter;
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 Append {
    public static void main(String[] args) throws IOException {
        Path path = Path.of("log.txt");
        Files.deleteIfExists(path);

        // 1) FileWriter: 第3引数(文字コードなしなら第2引数)に true
        try (BufferedWriter bw = new BufferedWriter(
                new FileWriter("log.txt", StandardCharsets.UTF_8, true))) {
            bw.write("1: FileWriter(…, true)");
            bw.newLine();
        }
        // 2) Files.writeString: CREATE と APPEND
        Files.writeString(path, "2: Files.writeString\n",
                StandardOpenOption.CREATE, StandardOpenOption.APPEND);
        // 3) Files.write: 行のリストを追記
        Files.write(path, List.of("3: Files.write"), StandardCharsets.UTF_8,
                StandardOpenOption.CREATE, StandardOpenOption.APPEND);
        // 4) Files.newBufferedWriter: 何行も続けて追記
        try (BufferedWriter bw = Files.newBufferedWriter(path, StandardCharsets.UTF_8,
                StandardOpenOption.CREATE, StandardOpenOption.APPEND)) {
            bw.write("4: Files.newBufferedWriter");
            bw.newLine();
        }
        System.out.print(Files.readString(path));
    }
}
$ java Append.java
1: FileWriter(…, true)
2: Files.writeString
3: Files.write
4: Files.newBufferedWriter

4つとも、前の内容を残したまま末尾に足されています。ポイントは次の2つです。

  • FileWriter は末尾の引数に true。new FileWriter("log.txt", true)(文字コードなし)または new FileWriter("log.txt", 文字コード, true) と書きます。ファイルがなければ新しく作られます。
  • Files 系は CREATE と APPEND を並べる。APPEND だけだと、ファイルがないときに例外になります(次の節で確かめます)。

追記で意外とつまずくのが改行です。前回の書き込みが改行で終わっていないと、次の追記が同じ行の続きに書かれます。追記する文字列は末尾を改行で終えると決めておくと、行がつながりません。

1行ごとに開き直すか、開いたままにするか

Files.writeString(..., APPEND) は呼ぶたびにファイルを開いて閉じます。2万行を「1行ごとに Files.writeString で追記」した場合と、「Files.newBufferedWriter を開いたままループで書く」場合の時間を3回計りました。

回 1行ごとに Files.writeString(APPEND) BufferedWriter を開いたまま
1回目 165 ms 41 ms
2回目 178 ms 7 ms
3回目 84 ms 6 ms

たまに1行足す用途なら Files.writeString の手軽さが勝ちますが、ループで大量に書くときは BufferedWriter を開いたままにするほうが大幅に速いことがわかります。数値は計測した環境での値で、マシンによって変わります。

StandardOpenOptionの組み合わせと挙動

Files.writeString、Files.write、Files.newBufferedWriter は、末尾の引数に StandardOpenOption を並べて開き方を指定します。公式ドキュメントの説明を要約すると次のとおりです。

オプション 意味
WRITE 書き込み用に開く
CREATE ファイルがなければ作る(あればそのまま開く)
CREATE_NEW 新しく作る。すでにあれば例外を投げる
APPEND 先頭ではなく末尾に書く
TRUNCATE_EXISTING すでにあるファイルの長さを 0 にしてから書く

オプションを1つも渡さない場合は、CREATE、TRUNCATE_EXISTING、WRITE を指定したのと同じ、つまり「なければ作り、あれば空にして書く」になります。

組み合わせごとの挙動を、「ファイルがない状態」と「ABCDEF と書かれたファイルがある状態」の両方で確かめます。どちらの状態にも xy を書き込み、結果の中身か、発生した例外の名前を表示するプログラムです。

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.OpenOption;
import java.nio.file.Path;
import java.util.LinkedHashMap;
import java.util.Map;

import static java.nio.file.StandardOpenOption.*;

public class Options {
    public static void main(String[] args) throws IOException {
        Map<String, OpenOption[]> cases = new LinkedHashMap<>();
        cases.put("指定なし", new OpenOption[]{});
        cases.put("WRITE", new OpenOption[]{WRITE});
        cases.put("CREATE", new OpenOption[]{CREATE});
        cases.put("APPEND", new OpenOption[]{APPEND});
        cases.put("CREATE, APPEND", new OpenOption[]{CREATE, APPEND});
        cases.put("TRUNCATE_EXISTING", new OpenOption[]{TRUNCATE_EXISTING});
        cases.put("CREATE, TRUNCATE_EXISTING", new OpenOption[]{CREATE, TRUNCATE_EXISTING});
        cases.put("CREATE_NEW", new OpenOption[]{CREATE_NEW});
        cases.put("APPEND, TRUNCATE_EXISTING", new OpenOption[]{APPEND, TRUNCATE_EXISTING});

        Path path = Path.of("opt.txt");
        for (var e : cases.entrySet()) {
            // ファイルがない状態
            Files.deleteIfExists(path);
            String none = tryWrite(path, e.getValue());
            // "ABCDEF" が入っている状態
            Files.writeString(path, "ABCDEF");
            String exists = tryWrite(path, e.getValue());
            System.out.printf("%-26s| ない: %-24s| ある: %s%n", e.getKey(), none, exists);
        }
    }

    static String tryWrite(Path path, OpenOption[] options) {
        try {
            Files.writeString(path, "xy", options);
            return "[" + Files.readString(path) + "]";
        } catch (Exception ex) {
            return ex.getClass().getSimpleName();
        }
    }
}

実行結果を表に整理しました(角かっこの中が、書き込み後のファイルの中身です)。

指定したオプション ファイルがないとき ABCDEF があるとき
指定なし [xy](作られる) [xy](空にして書く)
WRITE NoSuchFileException [xyCDEF](先頭から重ね書き)
CREATE [xy] [xyCDEF](先頭から重ね書き)
APPEND NoSuchFileException [ABCDEFxy](末尾に追記)
CREATE, APPEND [xy] [ABCDEFxy](末尾に追記)
TRUNCATE_EXISTING NoSuchFileException [xy]
CREATE, TRUNCATE_EXISTING [xy] [xy]
CREATE_NEW [xy] FileAlreadyExistsException
APPEND, TRUNCATE_EXISTING IllegalArgumentException IllegalArgumentException

この表から読み取れる、覚えておきたい点は3つです。

  1. CREATE だけでは上書きにならない。すでにあるファイルを空にせず、先頭から重ねて書くので、元の内容の後ろ側(CDEF)が残ります。オプションを自分で指定して上書きしたいときは、CREATE と TRUNCATE_EXISTING を並べます。
  2. APPEND だけ、TRUNCATE_EXISTING だけでは、ファイルがないときに NoSuchFileException。ファイルがなくても動くようにするには CREATE を足します。
  3. APPEND と TRUNCATE_EXISTING は同時に使えない。指定すると IllegalArgumentException になります。

よく使う組み合わせは、実質この3つです。

目的 指定
上書き(なければ作る) 指定なし
追記(なければ作る) CREATE, APPEND
新規作成のみ(あれば止める) CREATE_NEW

書き込み時に出会う例外と直し方

import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;

public class Errors {
    public static void main(String[] args) throws IOException {
        Path exists = Path.of("exists.txt");
        Files.writeString(exists, "最初の内容\n");

        // 1) CREATE_NEW で、すでにあるファイルを指定
        try {
            Files.writeString(exists, "上書き\n", StandardOpenOption.CREATE_NEW);
        } catch (IOException e) {
            System.out.println("1) " + e);
        }
        System.out.print("   中身は元のまま: " + Files.readString(exists));

        // 2) 親ディレクトリがない
        Path deep = Path.of("out/2026/report.txt");
        try {
            Files.writeString(deep, "レポート\n");
        } catch (IOException e) {
            System.out.println("2) " + e);
        }

        // 3) 同じことを FileWriter で
        try (FileWriter fw = new FileWriter("out/2026/report.txt")) {
            fw.write("レポート\n");
        } catch (IOException e) {
            System.out.println("3) " + e);
        }

        // 4) APPEND だけで、ファイルがない
        try {
            Files.writeString(Path.of("nothing.txt"), "追記\n", StandardOpenOption.APPEND);
        } catch (IOException e) {
            System.out.println("4) " + e);
        }

        // 直し方: 先に親ディレクトリを作る
        Files.createDirectories(deep.getParent());
        Files.writeString(deep, "レポート\n");
        System.out.print("5) 作成後: " + deep + " = " + Files.readString(deep));
    }
}
$ java Errors.java
1) java.nio.file.FileAlreadyExistsException: exists.txt
   中身は元のまま: 最初の内容
2) java.nio.file.NoSuchFileException: out/2026/report.txt
3) java.io.FileNotFoundException: out/2026/report.txt (No such file or directory)
4) java.nio.file.NoSuchFileException: nothing.txt
5) 作成後: out/2026/report.txt = レポート
例外 起きる場面 直し方
FileAlreadyExistsException CREATE_NEW で、すでにあるファイルを指定した 意図どおりなら catch して案内を出す。上書きしたいならオプションを外す
NoSuchFileException 親ディレクトリがない/APPEND だけでファイルがない Files.createDirectories(path.getParent()) を先に呼ぶ/CREATE を足す
FileNotFoundException FileWriter で親ディレクトリがない 同じく、先に親ディレクトリを作る
UnmappableCharacterException 指定した文字コードで表せない文字を書いた 文字コードを見直す(後述)

書き込みの NoSuchFileException は「書こうとしたファイルがない」ではなく「途中のフォルダがない」ことが多い、と覚えておくと原因にたどり着きやすくなります。Java はファイルを作るときに、途中のフォルダまでは自動で作りません。Files.createDirectories() は、すでにフォルダがあっても例外にならないので、書き込みの前に毎回呼んでも問題ありません。

CREATE_NEW は、公式ドキュメントで「ファイルがあるかの確認と作成が、ほかのファイル操作に対して不可分(アトミック)に行われる」と説明されています。Files.exists() で調べてから書く方法と違い、確認と作成の間に別の処理が割り込む余地がないので、「先に作った側だけが書ける」という排他の目印に使えます。

改行コード:newLine()と\nの違い

newLine() と、文字列に書く \n は、同じに見えて役割が違います。

書き方 出力される改行
bw.newLine() 実行している OS の改行(システムプロパティ line.separator の値)
"\n" を書く 常に LF(0x0A)
pw.println()、printf の %n OS の改行
Files.write(path, 行のリスト) OS の改行
テキストブロック(""" で囲む複数行文字列)内の改行 常に LF

5通りの書き方で A と B の2行を書き、od -c でバイト列を確かめます。

import java.io.BufferedWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;

public class Nl {
    public static void main(String[] args) throws IOException {
        try (BufferedWriter bw = Files.newBufferedWriter(Path.of("nl_newline.txt"))) {
            bw.write("A");
            bw.newLine();
            bw.write("B");
            bw.newLine();
        }
        try (BufferedWriter bw = Files.newBufferedWriter(Path.of("nl_n.txt"))) {
            bw.write("A\n");
            bw.write("B\n");
        }
        try (PrintWriter pw = new PrintWriter(Files.newBufferedWriter(Path.of("nl_println.txt")))) {
            pw.println("A");
            pw.printf("B%n");
        }
        Files.write(Path.of("nl_lines.txt"), List.of("A", "B"));
        Files.writeString(Path.of("nl_textblock.txt"), """
                A
                B
                """);
    }
}

Linux でそのまま実行すると、5つとも同じ結果(LF)になります。

$ java Nl.java
$ od -c nl_newline.txt
0000000   A  \n   B  \n
0000004

違いが出るのは、OS の改行が CRLF の場合です。Windows と同じ改行設定を再現するため、line.separator を CRLF にして同じプログラムを実行しました。

$ javac -d cls Nl.java
$ java -Dline.separator=$'\r\n' -cp cls Nl
$ od -c nl_newline.txt
0000000   A  \r  \n   B  \r  \n
0000006
$ od -c nl_n.txt
0000000   A  \n   B  \n
0000004
$ od -c nl_println.txt
0000000   A  \r  \n   B  \r  \n
0000006
$ od -c nl_lines.txt
0000000   A  \r  \n   B  \r  \n
0000006
$ od -c nl_textblock.txt
0000000   A  \n   B  \n
0000004

newLine()、println()、%n、Files.write は CRLF に変わり、\n とテキストブロックは LF のままです。選び方は次のとおりです。

目的 書き方
実行する OS に合わせたい(利用者がメモ帳などで開くファイル) newLine()、println()、%n
どの OS で実行しても LF にそろえたい(サーバーで扱うデータ、Git で管理するファイル) "\n" を書く
どの OS で実行しても CRLF にそろえたい(Windows 向けの CSV) "\r\n" を書く

出力先で求められる改行が決まっているなら、newLine() に任せず文字列で明示すると、実行する OS が変わっても同じファイルになります。

文字コードを指定して書く

Shift_JISで出力する

表計算ソフトで開く CSV など、Shift_JIS で渡す必要があるファイルは、文字コードを引数で指定します。改行は CRLF を明示しています。

import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;

public class Sjis {
    public static void main(String[] args) throws IOException {
        Charset sjis = Charset.forName("Shift_JIS");
        Path path = Path.of("sjis.csv");
        try (BufferedWriter bw = Files.newBufferedWriter(path, sjis)) {
            bw.write("品名,数量\r\n");
            bw.write("備品A,10\r\n");
        }
        System.out.println("サイズ: " + Files.size(path) + " バイト");
        System.out.print(Files.readString(path, sjis));
    }
}
$ java Sjis.java
サイズ: 21 バイト
品名,数量
備品A,10
$ od -c sjis.csv
0000000 225   i 226 274   , 220 224 227 312  \r  \n 224 365 225   i   A
0000020   ,   1   0  \r  \n
0000025

漢字1文字が2バイトで書かれ(UTF-8 なら3バイト)、行末が \r \n になっていることを確認できます。

表せない文字を書いたときの違い

Shift_JIS には、丸付き数字や一部の人名漢字、絵文字が含まれていません。そうした文字を書いたときの挙動は、Files 系と FileWriter で分かれます。

import java.io.FileWriter;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;

public class Unmap {
    public static void main(String[] args) throws IOException {
        String text = "①髙橋🍣\n";   // 丸付き数字、はしご高、絵文字
        for (String name : new String[]{"Shift_JIS", "MS932"}) {
            Charset cs = Charset.forName(name);
            System.out.println("--- " + name + "(①=" + cs.newEncoder().canEncode('①')
                    + " 髙=" + cs.newEncoder().canEncode('髙') + ")");
            try {
                Files.writeString(Path.of("unmap_files.txt"), text, cs);
                System.out.println("Files.writeString: 書き込めた");
            } catch (IOException e) {
                System.out.println("Files.writeString: " + e);
            }
            try (FileWriter fw = new FileWriter("unmap_fw.txt", cs)) {
                fw.write(text);
            }
            System.out.print("FileWriter       : " + Files.readString(Path.of("unmap_fw.txt"), cs));
        }
    }
}
$ java Unmap.java
--- Shift_JIS(①=false 髙=false)
Files.writeString: java.nio.charset.UnmappableCharacterException: Input length = 1
FileWriter       : ??橋?
--- MS932(①=true 髙=true)
Files.writeString: java.nio.charset.UnmappableCharacterException: Input length = 2
FileWriter       : ①髙橋?
観点 Files.writeString FileWriter
表せない文字を書いたとき UnmappableCharacterException を投げる 「?」に置き換えて書き込む
気づきやすさ その場で止まるのですぐわかる ファイルを開くまで気づきにくい

また、Shift_JIS では「①」「髙」を表せず、MS932(Windows の Shift_JIS 拡張。正式名は windows-31j)なら表せることもわかります。Windows のアプリケーションで開くファイルは MS932 を指定しておくと、扱える文字が広がります。絵文字はどちらでも表せないので、書き込む前に取り除くか、UTF-8 で出力できないかを検討します。事前に確かめたいときは、上のコードのように cs.newEncoder().canEncode(文字) が使えます。

JDK 18以降はデフォルトがUTF-8になった

JDK 18 で導入された JEP 400「UTF-8 by Default」により、標準 API のデフォルト文字セットが UTF-8 に統一されました。JEP 400 が影響を受ける API として挙げている中に、FileWriter、OutputStreamWriter、PrintStream が含まれます。一方、java.nio.file.Files の読み書きメソッドは、文字コードの引数がなければ常に UTF-8 を使う仕様です。

文字コードを書かずに「備品」の2文字を書き、ファイルの大きさを比べます。

import java.io.FileWriter;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;

public class Def {
    public static void main(String[] args) throws IOException {
        System.out.println("defaultCharset = " + Charset.defaultCharset());
        try (FileWriter fw = new FileWriter("def_fw.txt")) {   // 文字コードの指定なし
            fw.write("備品");
        }
        Files.writeString(Path.of("def_files.txt"), "備品");      // 文字コードの指定なし
        System.out.println("FileWriter       : " + Files.size(Path.of("def_fw.txt")) + " バイト");
        System.out.println("Files.writeString: " + Files.size(Path.of("def_files.txt")) + " バイト");
    }
}
$ javac -d cls Def.java
$ java -cp cls Def
defaultCharset = UTF-8
FileWriter       : 6 バイト
Files.writeString: 6 バイト

JDK 21 の既定では、どちらも UTF-8(3バイト×2文字)です。次に、デフォルト文字セットが windows-31j になっている状態を、検証用に起動オプションで作って同じプログラムを動かします。

$ java -Dfile.encoding=MS932 -cp cls Def
defaultCharset = windows-31j
FileWriter       : 4 バイト
Files.writeString: 6 バイト

文字コードを書いていない FileWriter は、デフォルト文字セットに合わせて出力が変わり、Files.writeString は変わりません。 JEP 400 は、主な影響はアジア圏のロケールで Windows を使っている利用者に及ぶと述べています。JDK 17 以前で動いていた「new FileWriter(名前) で Shift_JIS のファイルが出力されていた」処理は、JDK 18 以降では UTF-8 で出力されることになります。対処は次の順で進めるとよいでしょう。

手順 内容
1 new FileWriter(名前)、new OutputStreamWriter(出力)、new PrintWriter(名前) など、文字コードなしの箇所を探す
2 出力先が求める文字コードを確かめ、引数で明示する
3 すぐに直せない場合は、JEP 400 が用意している -Dfile.encoding=COMPAT で、当面 JDK 17 以前と同じ決め方に合わせる
4 新しく書くコードは、最初から文字コードを引数で渡す

closeし忘れると中身が空になる

BufferedWriter は文字をメモリにためてから書くので、ためたまま閉じずに終わると、ファイルに届かないことがあります。

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

public class NoClose {
    public static void main(String[] args) throws IOException {
        BufferedWriter bw = new BufferedWriter(new FileWriter("noclose.txt"));
        bw.write("備品A,10");
        bw.newLine();
        // close() も flush() も呼ばずに終了する
        System.out.println("書き込み直後のサイズ: " + Files.size(Path.of("noclose.txt")) + " バイト");
    }
}
$ java NoClose.java
書き込み直後のサイズ: 0 バイト
$ ls -l noclose.txt
-rw-r--r-- 1 taro taro 0 Oct  3 23:48 noclose.txt

(ls の所有者の表示は環境に合わせて置き換えています。)

プログラムが終わったあとも、ファイルは 0 バイトのままです。例外も出ないので、「ファイルはできているのに中身が空」という状態になります。flush() と close() の働きを順に確かめます。

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

public class Flush {
    public static void main(String[] args) throws IOException {
        Path path = Path.of("flush.txt");
        try (BufferedWriter bw = new BufferedWriter(new FileWriter(path.toFile()))) {
            bw.write("備品A,10");
            bw.newLine();
            System.out.println("write 直後 : " + Files.size(path) + " バイト");
            bw.flush();
            System.out.println("flush 後   : " + Files.size(path) + " バイト");
            bw.write("備品B,25");
            bw.newLine();
        }
        System.out.println("close 後   : " + Files.size(path) + " バイト");
    }
}
$ java Flush.java
write 直後 : 0 バイト
flush 後   : 11 バイト
close 後   : 22 バイト
メソッド 働き
write() バッファにためる(この時点ではファイルに届いていないことがある)
flush() ためた分をファイルへ送る。ストリームは開いたまま
close() flush してから閉じる。以降は書き込めない

公式ドキュメントでも、close() は「先に flush してからストリームを閉じる」と説明されています。つまり try-with-resources で書いていれば、flush() を自分で呼ばなくても中身は書き込まれます。flush() を明示的に呼ぶのは、ログのように「開いたまま、途中経過もすぐファイルに反映したい」場合です。

「ファイルが空になる」ときの確認項目をまとめます。

確認すること 対処
try-with-resources を使っているか try (BufferedWriter bw = ...) { } の形にする
途中で return や例外で抜けていないか try-with-resources なら、その場合も自動で閉じられる
追記のつもりが上書きになっていないか APPEND(FileWriter なら true)を指定する
同じファイルを複数箇所で開いていないか 開く場所を1か所にまとめる

よくある質問

Q. BufferedWriter を close すれば、内側の FileWriter も閉じられますか? A. はい。外側の BufferedWriter や PrintWriter を閉じれば、包んでいる Writer も一緒に閉じられます。try-with-resources の丸かっこには、いちばん外側の1つを書けば足ります。

Q. Files.write と Files.writeString はどう違いますか? A. Files.write は行のリスト(またはバイト配列)を受け取り、行のリストの場合は各行の後ろに OS の改行を付けます。Files.writeString は文字列をそのまま書き、改行は足しません。JDK 8 で使えるのは Files.write です。

Q. BOM 付きの UTF-8 で出力するには? A. 標準 API に BOM を自動で付ける指定はないので、最初に U+FEFF の1文字を自分で書き込みます。UTF-8 では EF BB BF の3バイトとして出力されます。

Q. 追記モードで開いたとき、ファイルがなければどうなりますか? A. new FileWriter(名前, true) は、ファイルがなければ新しく作って書き込みます。Files 系で APPEND だけを指定した場合は NoSuchFileException になるので、CREATE を並べて指定します。

まとめ

  • 短い内容は Files.writeString、ループで何行も書くなら Files.newBufferedWriter を try-with-resources で。どちらも文字コードを省くと UTF-8
  • 追記は、FileWriter なら末尾の引数に true、Files 系なら CREATE と APPEND。APPEND だけではファイルがないときに NoSuchFileException
  • CREATE だけでは上書きにならず、先頭から重ね書きになる。上書きは指定なし、新規作成のみは CREATE_NEW
  • 親ディレクトリがないと NoSuchFileException/FileNotFoundException。先に Files.createDirectories() を呼ぶ
  • newLine()・println()・%n は OS の改行、\n は常に LF。出力先の改行が決まっているなら文字列で明示する
  • 表せない文字は、Files 系は例外、FileWriter は「?」に置き換え。Windows 向けの Shift_JIS は MS932 が扱える文字が広い
  • JDK 18 以降、文字コードなしの FileWriter は UTF-8 で出力する。文字コードは引数で明示しておく

参考・出典

  • Java SE 21 API ドキュメント:java.nio.file.Files、java.nio.file.StandardOpenOption、java.io.BufferedWriter(Oracle)
  • JEP 400: UTF-8 by Default(OpenJDK)

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Java ファイル書き込み|BufferedWriterと追記・文字コード」

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?