はじめに
java.nio.file.Files には Reader や Writer 用のスタティックファクトリメソッドが用意されています。
Reader reader = Files.newBufferedReader(path, charset); Writer writer = Files.newBufferedWriter(path, charset);
これは以下のような、Reader Writer 生成と同等なコンビニエンス・メソッドと思われがちですが、挙動に違いがあります。
Reader reader = new BufferedReader( new InputStreamReader(Files.newInputStream(path), charset)); Writer writer = new BufferedWriter( new OutputStreamWriter(Files.newOutputStream(path), charset));
エンコード・エラーが発生した場合、前者は java.nio.charset.UnmappableCharacterException となり、後者は対象文字が ? に置換されます。
以下のようにすれば、同じ挙動になります。
Reader reader = new BufferedReader( new InputStreamReader(Files.newInputStream(path), charset.newEncoder())); Writer writer = new BufferedWriter( new OutputStreamWriter(Files.newOutputStream(path), charset.newEncoder()));
Files.newBufferedWriter() は何が違う?
Writer を例に違いを見ていきましょう。
最初に Files.newBufferedWriter(path, charset) は、以下のように cs.newEncoder() でエンコーダ(CharsetEncoder)を取得して OutputStreamWriter を生成しています。
public static BufferedWriter newBufferedWriter( Path path, Charset cs, OpenOption... options) throws IOException { CharsetEncoder encoder = cs.newEncoder(); Writer writer = new OutputStreamWriter(newOutputStream(path, options), encoder); return new BufferedWriter(writer); }
OutputStreamWriter は以下のように StreamEncoder を初期化します。
public OutputStreamWriter(OutputStream out, CharsetEncoder enc) { super(out); if (enc == null) throw new NullPointerException("charset encoder"); se = StreamEncoder.forOutputStreamWriter(out, this, enc); }
一方、OutputStreamWriter() を Charset 指定でインスタンス化する場合は以下のようになっており、
public OutputStreamWriter(OutputStream out, Charset cs) { super(out); if (cs == null) throw new NullPointerException("charset"); se = StreamEncoder.forOutputStreamWriter(out, this, cs); }
StreamEncoder は以下のように初期化されます。
public static StreamEncoder forOutputStreamWriter( OutputStream out, Object lock, CharsetEncoder enc){ return new StreamEncoder(out, lock, enc); }
private StreamEncoder(OutputStream out, Object lock, Charset cs) { this(out, lock, cs.newEncoder() .onMalformedInput(CodingErrorAction.REPLACE) .onUnmappableCharacter(CodingErrorAction.REPLACE)); }
cs.newEncoder() でエンコーダを取得した後、onMalformedInput() と onUnmappableCharacter() でエンコーダのエラー発生時の挙動を設定しています。
これが挙動の違いとなります。
CodingErrorAction には以下の3つが定義されています。
| CodingErrorAction | 説明 |
|---|---|
IGNORE |
エンコード/デコード・エラー入力を破棄 |
REPLACE |
エンコード/デコード・エラー入力を破棄し、出力バッファに置換値を追加 |
REPORT |
エンコード/デコード・エラーを報告する |
CharsetEncoder や CharsetDecoder のデフォルト値は以下のように REPORT となっています。
public abstract class CharsetEncoder{ private CodingErrorAction malformedInputAction = CodingErrorAction.REPORT; private CodingErrorAction unmappableCharacterAction = CodingErrorAction.REPORT; }
Files.newBufferedWriter() ではデフォルトの REPORT により、エンコード・エラーで java.nio.charset.UnmappableCharacterException がスローされますが、Charset 指定で作成した OutputStreamWriter は REPLACE により ? への置き換えが行われます。
MalformedInputException と UnmappableCharacterException
エンコード・エラーには一般的な2種類のエラーがあります。
| エンコード・エラー | 説明 |
|---|---|
MalformedInputException |
入力文字シーケンスが正当な16ビットUnicodeシーケンスでない場合 |
UnmappableCharacterException |
入力文字シーケンスは正当でも、これを指定された文字セット内の有効なバイト・シーケンスにマップできない場合 |
以下のように REPORT が指定されているケース(指定しない場合と同様)では、
cs.newEncoder()
.onMalformedInput(CodingErrorAction.REPORT)
.onUnmappableCharacter(CodingErrorAction.REPORT));
エンコード結果が CoderResult として報告され、以下のメソッドから例外がスローされる流れになっています。
public final class CoderResult { public void throwException() throws CharacterCodingException { switch (type) { case CR_UNDERFLOW: throw new BufferUnderflowException(); case CR_OVERFLOW: throw new BufferOverflowException(); case CR_MALFORMED: throw new MalformedInputException(length); case CR_UNMAPPABLE: throw new UnmappableCharacterException(length); default: assert false; } } }
まとめ
java.nio.file.Files の newBufferedReader() や newBufferedWriter() は、昔ながらの方法で Reader Writer を生成した場合とは、エンコード・エラー発生時の挙動が異なります。
リファクタリングなどを行う際には注意が必要となります。