Path file = Paths.get("New Text Document.txt");
try {
    System.out.println(Files.readString(file, StandardCharsets.UTF_8));
    System.out.println(Files.readString(file, StandardCharsets.UTF_16));
} catch (Exception e) {
    System.out.println("yep it's an exception");
}

可能会让步

some text
Exception in thread "main" java.lang.Error: java.nio.charset.MalformedInputException: Input length = 1
    at java.base/java.lang.String.decodeWithDecoder(String.java:1212)
    at java.base/java.lang.String.newStringNoRepl1(String.java:786)
    at java.base/java.lang.String.newStringNoRepl(String.java:738)
    at java.base/java.lang.System$2.newStringNoRepl(System.java:2390)
    at java.base/java.nio.file.Files.readString(Files.java:3369)
    at test.Test2.main(Test2.java:13)
Caused by: java.nio.charset.MalformedInputException: Input length = 1
    at java.base/java.nio.charset.CoderResult.throwException(CoderResult.java:274)
    at java.base/java.lang.String.decodeWithDecoder(String.java:1205)
    ... 5 more

这个错误"不应该发生".以下是java.lang.String种方法:

private static int decodeWithDecoder(CharsetDecoder cd, char[] dst, byte[] src, int offset, int length) {
    ByteBuffer bb = ByteBuffer.wrap(src, offset, length);
    CharBuffer cb = CharBuffer.wrap(dst, 0, dst.length);
    try {
        CoderResult cr = cd.decode(bb, cb, true);
        if (!cr.isUnderflow())
            cr.throwException();
        cr = cd.flush(cb);
        if (!cr.isUnderflow())
            cr.throwException();
    } catch (CharacterCodingException x) {
        // Substitution is always enabled,
        // so this shouldn't happen
        throw new Error(x);
    }
    return cb.position();
}

编辑:正如@user16320675所指出的,当一个字符数为奇数的UTF-8文件被读取为UTF-16时,就会发生这种情况.对于偶数个字符,ErrorMalformedInputException都不会发生.为什么是Error

推荐答案

这里发生了不同的事情.但是,是的,看起来你真的found a JVM bug!岁了,我想:)

但是,需要一些上下文来准确地解释发生了什么以及你发现了什么.我认为你的代码有更大的问题是你自己造成的,一旦你解决了这些问题,JVM错误将不再是你的问题(但是,无论如何,一定要报告它!).我会尽力解决所有问题:

  1. 由于UTF-8和UTF-16基本上不兼容,您的代码被 destruct .结果是,将偶数个字符保存为UTF-8可能会导致使用UTF-16读取某些内容时不会出错,尽管您所读取的内容将完全是胡说八道.如果字符数为奇数,则会遇到解码错误.

  2. JVM有问题!您发现了一个JVM错误——解码错误的影响应该是not,而不是抛出Error.具体的缺陷是,替换实际上并没有覆盖所有的故障条件,但编写代码时假设它会覆盖所有的故障条件.

  3. 该漏洞似乎与不恰当地应用宽松模式有关,这需要解释什么是替代和下溢.

UTF-8与UTF-16

  • 将字符转换为字节或将字符转换为字节时,使用的是字符集编码.
  • 文件是字节序列,而不是字符.
  • 这些规则没有例外.

因此,如果您输入字符并保存,而不是 Select 字符集编码?有人是.如果你在键盘上输入notepad.exe并保存,那么记事本会为你 Select 一个.你不能没有编码.

为了解释这里发生的事情的细微差别,请暂时忘掉编程.

我们决定一个方案:你用一个形容词来描述一个人;你把它写在一张纸上(只是形容词),然后交给我.然后我读了它,猜猜你想描述的是我们的朋友圈.我碰巧会说两种语言,能说流利的荷兰语和英语.你不知道,或者你知道,但我们从未讨论过我们之间协议的这一部分.

你开始思考一个特别瘦长的人,于是你决定在便条上写下"slim".你离开房间,我进go ,我拿起便条.

我做了一个错误的假设,我假设你是用荷兰语写的,所以我读了这张便条,以为你是用荷兰语写的,我读了"slim",它是is an actual dutch word,但它的意思是"smart".如果你在 sticky 上写下,比如说"tall",这就不会发生:"tall"不在荷兰语词典中,因此我知道你犯了一个"错误"(你写了一个无效的词.它对你来说是有效的,但我读它时假设它是荷兰语,所以我认为你犯了一个错误).但是,"slim"这四个字母正好是荷兰语和英语的两个字母,但它的意思完全不同.

UTF-8和UTF-16是完全一样的:有一些字符序列可以用UTF-16编码,产生字节流,这恰好也是完全有效的UTF-8,但它意味着完全不同的东西,反之亦然!但也有一些字符序列,如果保存为UTF-16,然后读取为UTF-8(反之亦然),则这些字符序列将无效.

因此,可能出现"苗条"的情况,也可能出现"高大"的情况.其中任何一个对你来说都是无用的:当我读了你的便条,看到"苗条",我认为这意味着"聪明",我们仍然"迷路",我选错了朋友——没有更好的结果.这有什么意义,对吗?每当你把字符转换成字节,然后再转换回来,路径上的每一个转换步骤都需要对所有的beforehand个字符使用完全相同的编码,否则它永远不会工作.

但它是如何失败的——这就是问题所在:当你写《苗条》时,我选错了朋友.当你写"Toll"时,我惊呼说发生了一个错误,因为那不是荷兰语单词.

UTF-16根据字符将每个字符转换为2、3或4字节的序列.当您将普通的简ascii字符保存为UTF-8时,它们最终都是1个字节,通常任何2个这样的字节,解码为单个UTF-16字符,"都是有效的"(但是一个完全不同的字符,与输入完全无关!),因此,如果将8个ASCII字符保存为UTF-8(或ASCII,归结为相同的字节流),然后将其读取为UTF-16,则很可能不会引发任何异常.不过,你会得到一条4长度的gobbledygook.

让我们试试看!

String test = "gerikg";
byte[] saveAsUtf8 = test.getBytes(StandardCharsets.UTF_8);
String readAsUtf16 = new String(saveAsUtf8, StandardCharsets.UTF_16);
System.out.println(test);
System.out.println(readAsUtf16);

... results in:

gerikg
来物歧

看见一个完全不相关的汉字出现了.

但是,现在让我们来看一个奇数:

String test = "gerikgw";
byte[] saveAsUtf8 = test.getBytes(StandardCharsets.UTF_8);
String readAsUtf16 = new String(saveAsUtf8, StandardCharsets.UTF_16);
System.out.println(test);
System.out.println(readAsUtf16);

gerikgw
来物歧�

请注意奇怪的问号:这是一个字形(字形是字体中的一个条目:用来表示某个字符的符号),它表示:这里出了问题——这不是一个真实的字符,而是解码错误.

但是,在一个文本文件中输入gerikgw(确保它没有尾随回车符,因为这也是一个符号),然后运行您的代码,事实上——JVM错误!很好的发现!

替代

那个奇怪的问号符号是"替代".UTF编码器可以对任何32位值进行编码.unicode系统有32位的可寻址字符(实际上,不完全是这样,它更少,一些插槽被故意标记为未使用,并且永远不会被使用,出于有趣的原因,但与此无关),但并不是所有可用的插槽都是"填充"的.如果我们以后需要新角色的话,还有空间.此外,并非每个字节序列都必须是有效的UTF-8.

那么,当检测到"无效"输入时该怎么办?在严格的解析模式下,一个选项是崩溃(抛出一些东西).另一种方法是将错误作为"错误"字符"读取"(当您将其打印到屏幕上时,会显示该问号图示符),然后从我们结束的地方开始.UTF是一个非常酷的格式化系统,它"知道"一个新字符何时开始,因此,你永远不会遇到偏移问题(在这种情况下,我们"偏移了一半",并且由于未对齐而不断读取错误的内容).

JVM错误

这解释了您粘贴的代码:根据注释,格式错误的编码内容"不会发生",因为"宽松模式"处于启用状态,所以任何错误都只会导致替换.除了it is right there之外,这是一个非常愚蠢的错误,其中一个错误真的导致了这段代码的作者明显地、听到地拍了拍他们的额头,感到非常羞愧:

在这种情况下,在剩下的字节序列中只剩下一个字节,但在UTF-16世界中,所有有效的字节表示形式都至少有2个字节.这种情况称为underflow,解码器(CharsetDecoder cd)没有问题——它正确地检测到这种情况,因此if (!cr.isUnderflow()) cr.throwException();导致cr.throwException()被执行,这自然会抛出MalformedInputException,这是CharacterCodingException的一个子类型,因此,代码直接跳到catch 4行,下面说"这不可能发生".

结论是,作者有一个头脑放屁的时刻.只有两件事是真的:

  1. 这里永远不会出现下溢,永远不会.最愚蠢的是,里面有if个用来判断不可能的事情,这是毫无意义的.
  2. 此处可能出现下溢,因此catch块中的注释不正确.替代并不能解决这个问题.

正确的代码应该是:

private static int decodeWithDecoder(CharsetDecoder cd, char[] dst, byte[] src, int offset, int length) {
    ByteBuffer bb = ByteBuffer.wrap(src, offset, length);
    CharBuffer cb = CharBuffer.wrap(dst, 0, dst.length);
    try {
        CoderResult cr = cd.decode(bb, cb, true);
        if (!cr.isUnderflow())
            cr.throwException();
        cr = cd.flush(cb);
        if (!cr.isUnderflow()) cb.write(SUBSTITUTION_CHAR);
    } catch (CharacterCodingException x) {
        // 替代 is always enabled,
        // so this shouldn't happen
        throw new Error(x);
    }
    return cb.position();
}

换句话说,如果发生下溢,则发出一个substitution char(表示由不具有任何意义的悬空单字节表示的"un字符"),然后返回结果.毕竟,这符合宽松模式的策略, comments 说我们显然处于宽松模式("替代已启用").

我建议你在OpenJDK项目中提交一个bug,或者先搜索它.

直到它被修复...

解决办法

替换:

Files.readString(file, StandardCharsets.UTF_16);

与:

fixedReadString(file, StandardCharsets.UTF_16);

...

public static String fixedReadString(Path file, Charset charset) {
  try {
   Files.readString(file, StandardCharsets.UTF_16);
  } catch (Error e) {
    if (!(e.getCause() instanceof MalformedInputException)) throw e;
    // see notes
  }
}

剩下的一个问题是,当这种情况发生时,你想做什么.输入肯定有问题,我通常不喜欢"宽松"模式.所以我只写了throw new MalformedInputException个,通常都重写为使用严格模式.然而,如果你想复制intended效果(也就是:"来物歧�"——这不是很有用,但它可以复制代码应该返回的内容),那就不太容易重新创建了.你可以祈祷,只要在末尾添加一个随机字符(比如,一个空格)并重新解析,就有望至少生成something个,你可以重写Files.readString本身的全部功能(不是too复杂),或者只重写return "�";个——扔掉整个字符串,只留下一个替换字符,这至少可以帮助某人调试:啊,对,我用错了字符集来读取这个文件.

Java相关问答推荐

RxJava PublishSubject缓冲区元素超时

FALSE:它应该在什么时候使用?

如何调整工作时间日历中的时间

这是什么Java构造`(InputStream Is)->;()->;{}`

使用Spring Boot3.2和虚拟线程的并行服务调用

如何修复PDF重建过程中的文本定位

我可以在MacOS上使用什么Java函数来在适当的设备上以适当的音量播放适当的alert 声音?

Spring Boot&;Docker:无法执行目标org.springframework.boot:spring-boot-maven-plugin:3.2.0:build-image

使用While循环打印素数,无法正常工作

如何在@CsvSource中传递空格作为值

Spring动态反序列化JSON可以是列表,也可以只是一个对象

从泛型枚举创建EnumMap

在缺少字段时使用Jackson With Options生成Optional.Empty()

循环不起作用只有第一个元素重复

在Java中将.GRF转换为图像文件

rest api服务 spring 启动中出现IllegalFormatConversionException

如何使用Hibernate v6.2构建NamingStrategy,以表名作为所有列的前缀?

如何在JSP中从select中获取值并将其放入另一个select

单例模式中热切初始化和惰性初始化的区别

BigDecimal stripTrailingZeros 和相等