java.nio.file.Files.newBufferedWriter() で生成した Writer に関する注意点


はじめに

java.nio.file.Files には ReaderWriter 用のスタティックファクトリメソッドが用意されています。

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 エンコード/デコード・エラーを報告する

CharsetEncoderCharsetDecoder のデフォルト値は以下のように REPORT となっています。

public abstract class CharsetEncoder{
    private CodingErrorAction malformedInputAction = CodingErrorAction.REPORT;
    private CodingErrorAction unmappableCharacterAction = CodingErrorAction.REPORT;
}

Files.newBufferedWriter() ではデフォルトの REPORT により、エンコード・エラーで java.nio.charset.UnmappableCharacterException がスローされますが、Charset 指定で作成した OutputStreamWriterREPLACE により ? への置き換えが行われます。


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.FilesnewBufferedReader()newBufferedWriter() は、昔ながらの方法で Reader Writer を生成した場合とは、エンコード・エラー発生時の挙動が異なります。

リファクタリングなどを行う際には注意が必要となります。