Java: ファイル読むのを Files に移行するときの非互換

細かいけど。

こう思ってた

遥か昔、Java 1.1 の時代から、人類は InputStreamReader をひとつひとつ丁寧に手作りしてきた。

		// パターン A
		String path = "foo.txt";
		Charset charset = Charset.forName("Shift_JIS");

		try (BufferedReader br = new BufferedReader(
				new InputStreamReader(new FileInputStream(path), charset))) {
			br.lines().forEach(System.out::println);
		}

Java 7 で java.nio.file.Files クラスが追加されたときに、newBufferedReader(Path, Charset) という static メソッドがあって、「わぁいこれからはみじかくかけるぞ」と思ったものだ。

		// パターン B
		Path path = Paths.get("foo.txt");
		Charset charset = Charset.forName("Shift_JIS");

		try (BufferedReader br = Files.newBufferedReader(path, charset)) {
			br.lines().forEach(System.out::println);
		}

同じクラスに readAllLines(Path, Charset) というのもあって、小さなファイルならさらに短く書けると思った。

		// パターン C
		Path path = Paths.get("foo.txt");
		Charset charset = Charset.forName("Shift_JIS");

		Files.readAllLines(path, charset).forEach(System.out::println);

違ってた

上のサンプルでは Shift_JIS を指定してたけど、世の中には、

  • Windows-31J には含まれてるけど Shift_JIS には含まれていない文字
  • シフト JIS 的に正しくないオクテット列

というものがあって、自分で InputStreamReader を作ってファイルを読む場合は、U+FFFD に変換されていた。
その後、Java 1.4 で NIO が導入されて java.nio.charset.CharsetDecoder が追加されたとき、そういう場合にどうするかを選べるようになった。選択肢は java.nio.charset.CodingErrorAction に定数として定義されている。

定数 ざっくり説明
REPLACE U+FFFD に変換する
IGNORE 無視する
REPORT エラー扱いにする

InputStreamReader を自分で作る場合は REPLACE にあたる設定になっている。これに対し、Files クラスの newBufferedReader(Path, Charset) や readAllLines(Path, Charset) だと REPORT が使われて、不明な文字やオクテット列があると、例外が throw される。
この差異が問題になるかどうかはユースケースによるけど、大きなトラブルの元になることもあるかもしれない。

余談

Files クラスは Java 8 でメソッドが増えている。newBufferedReader(Path) とか readAllLines(Path) っていう、引数に Charset を取らないオーバーロードが追加されている。
java.io 時代のノリだと、プラットフォームのデフォルトエンコーディング使うのかな? と思えるけど、これも想像とは違ってて、固定で UTF-8 が使われる。なんでだろ。