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
сделал:
�
Что также имеет смысл.
Итак, мои вопросы:
- Каково ожидаемое поведение от Java-приложения в этом сценарии?
- Почему есть разница между использованием
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);
}
Так что они работают по-разному, но в документации нет никаких указаний на разницу. Это плохо документировано, но я полагаю, что они изменили функциональность, потому что вы скорее получите исключение, чем ваши данные будут молча повреждены.
Будьте осторожны при работе с кодировками символов!