- JAVA
Java ファイル書き込み|BufferedWriterと追記・文字コード
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つです。
CREATEだけでは上書きにならない。すでにあるファイルを空にせず、先頭から重ねて書くので、元の内容の後ろ側(CDEF)が残ります。オプションを自分で指定して上書きしたいときは、CREATEとTRUNCATE_EXISTINGを並べます。APPENDだけ、TRUNCATE_EXISTINGだけでは、ファイルがないときに NoSuchFileException。ファイルがなくても動くようにするにはCREATEを足します。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期連続の黒字決算。