StreamDecoder против InputStreamReader при чтении искаженных файлов

Я столкнулся с каким-то странным поведением при чтении файлов в Java 8, и мне интересно, может ли кто-то это понять.

Сценарий:

Чтение искаженного текстового файла. Под неправильным форматом я подразумеваю, что он содержит байты, которые не отображаются ни в какие кодовые точки Юникода.

Код, который я использую для создания такого файла, выглядит следующим образом:

byte[] text = new byte[1];
char k = (char) -60;
text[0] = (byte) k;
FileUtils.writeByteArrayToFile(new File("/tmp/malformed.log"), text);

Этот код создает файл, который содержит ровно один байт, который не является частью таблицы ASCII (и не расширенного).

Пытаться cat этот файл производит следующий вывод:

Который является символом замены ЮНИКОДА. Это имеет смысл, потому что UTF-8 требуется 2 байта для декодирования не-ascii символов, но у нас есть только один. Такое поведение я ожидаю и от своего Java-кода.

Вставка некоторого общего кода:

private void read(Reader reader) throws IOException {

    CharBuffer buffer = CharBuffer.allocate(8910);

    buffer.flip();

    // move existing data to the front of the buffer
    buffer.compact();

    // pull in as much data as we can from the socket
    int charsRead = reader.read(buffer);

    // flip so the data can be consumed
    buffer.flip();

    ByteBuffer encode = Charset.forName("UTF-8").encode(buffer);
    byte[] body = new byte[encode.remaining()];
    encode.get(body);

    System.out.println(new String(body));
}

Вот мой первый подход с использованием nio:

FileInputStream inputStream = new FileInputStream(new File("/tmp/malformed.log"));
read(Channels.newReader(inputStream.getChannel(), "UTF-8");

Это производит следующее исключение:

java.nio.charset.MalformedInputException: Input length = 1

    at java.nio.charset.CoderResult.throwException(CoderResult.java:281)
    at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:339)
    at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:178)
    at java.io.Reader.read(Reader.java:100)

Это не то, что я ожидал, но также имеет смысл, потому что это на самом деле испорченный и недопустимый файл, а исключение в основном говорит нам, что ожидается чтение большего количества байтов.

И мой второй (с помощью обычного java.io):

FileInputStream inputStream = new FileInputStream(new File("/tmp/malformed.log"));
read(new InputStreamReader(inputStream, "UTF-8"));

Это не дает сбоя и дает точно такой же результат, как cat сделал:

Что также имеет смысл.

Итак, мои вопросы:

  1. Каково ожидаемое поведение от Java-приложения в этом сценарии?
  2. Почему есть разница между использованием Channels.newReader (который возвращает StreamDecoder) и просто используя обычный InputStreamReader? Я делаю что-то не так с тем, как я читаю?

Любые разъяснения будут высоко оценены.

Спасибо:)

1 ответ

Решение

Разница между поведением на самом деле сводится к классам StreamDecoder и Charset. InputStreamReader получает CharsetDecoder от StreamDecoder.forInputStreamReader(..) который делает замену по ошибке

StreamDecoder(InputStream in, Object lock, Charset cs) {
    this(in, lock,
    cs.newDecoder()
    .onMalformedInput(CodingErrorAction.REPLACE)
    .onUnmappableCharacter(CodingErrorAction.REPLACE));
}

в то время как Channels.newReader(..) создает декодер с настройками по умолчанию (т. е. отчет вместо замены, что приводит к исключению в дальнейшем)

public static Reader newReader(ReadableByteChannel ch,
                               String csName) {
    checkNotNull(csName, "csName");
    return newReader(ch, Charset.forName(csName).newDecoder(), -1);
}

Так что они работают по-разному, но в документации нет никаких указаний на разницу. Это плохо документировано, но я полагаю, что они изменили функциональность, потому что вы скорее получите исключение, чем ваши данные будут молча повреждены.

Будьте осторожны при работе с кодировками символов!

Другие вопросы по тегам