这里发生了不同的事情.但是,是的,看起来你真的found a JVM bug!岁了,我想:)
但是,需要一些上下文来准确地解释发生了什么以及你发现了什么.我认为你的代码有更大的问题是你自己造成的,一旦你解决了这些问题,JVM错误将不再是你的问题(但是,无论如何,一定要报告它!).我会尽力解决所有问题:
由于UTF-8和UTF-16基本上不兼容,您的代码被 destruct .结果是,将偶数个字符保存为UTF-8可能会导致使用UTF-16读取某些内容时不会出错,尽管您所读取的内容将完全是胡说八道.如果字符数为奇数,则会遇到解码错误.
JVM有问题!您发现了一个JVM错误——解码错误的影响应该是not,而不是抛出Error
.具体的缺陷是,替换实际上并没有覆盖所有的故障条件,但编写代码时假设它会覆盖所有的故障条件.
该漏洞似乎与不恰当地应用宽松模式有关,这需要解释什么是替代和下溢.
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行,下面说"这不可能发生".
结论是,作者有一个头脑放屁的时刻.只有两件事是真的:
- 这里永远不会出现下溢,永远不会.最愚蠢的是,里面有
if
个用来判断不可能的事情,这是毫无意义的.
- 此处可能出现下溢,因此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 "�";
个——扔掉整个字符串,只留下一个替换字符,这至少可以帮助某人调试:啊,对,我用错了字符集来读取这个文件.