Java:单个代码点的 UTF-8 字节长度(再次代理)

Fra*_* D. 5 java utf-8 character-encoding

这一切都始于一个非常基本的问题:给定一个char——或者更确切地说,一个整数代码点,请参阅CharacterAPI——,返回其 UTF-8 编码所需的字节数。然而,我在这个无辜的小问题上花费的时间越多,它就变得越令人困惑。

我的第一个方法是:

int getUtf8ByteCount_stdlib(int codePoint) {
    int[] codePoints = { codePoint };
    String string = new String(codePoints, 0, 1);
    byte[] bytes = string.getBytes(StandardCharsets.UTF_8);
    return bytes.length;
}
Run Code Online (Sandbox Code Playgroud)

或者对于那些喜欢它的人:

int getUtf8ByteCount_obfuscated(int codePoint) {
    return new String(new int[] { codePoint }, 0, 1).getBytes(StandardCharsets.UTF_8).length;
}
Run Code Online (Sandbox Code Playgroud)

然后,为了简单性和效率,我创建了另一个版本(基于UTF-8 维基百科文章):

int getUtf8ByteCount_handRolled(int codePoint) {
    if (codePoint > 0x7FFFFFFF) {
        throw new IllegalArgumentException("invalid UTF-8 code point");
    }
    return codePoint <= 0x7F? 1
         : codePoint <= 0x7FF? 2
         : codePoint <= 0xFFFF? 3
         : codePoint <= 0x1FFFFF? 4
         : codePoint <= 0x3FFFFFF? 5
         : 6;
}
Run Code Online (Sandbox Code Playgroud)

经过多年与字符编码的许多可爱的微妙之处的斗争,我进行了测试,瞧!它失败了;对于从 '\uD800' 到 '\uDFFF' 的所有代码点,“stdlib”版本返回 1 个字节,而“hand-rolled”则返回 3 个字节。毫无疑问,是善良的替代角色再次造成了破坏!现在,根据我对那些讨厌的小家伙的理解,我会说第二个版本是正确的。我的问题:

  1. String.getBytes()or(Java的UTF-8实现)坏了,还是我的理解?(我使用的是 Oracle Java SE 运行时环境 1.6.0_22-b04)
  2. 即使不正确,它是否比“手卷”版本更好,因为它与 Java 的 UTF-8 生成的实际字节编码/解码更一致?
  3. 除了正确性考虑之外,Java 标准库是否提供了比我的“stlib”更干净的方法?

Tag*_*eev 2

问题在于,从 Java 的角度来看,由单个“代理”代码点组成的字符串根本不是有效的字符串。String.getBytes()JavaDoc 中描述了使用的编码器的默认行为:

此方法始终用此字符集的默认替换字节数组替换格式错误的输入和不可映射的字符序列。CharsetEncoder当需要对编码过程进行更多控制时,应使用该类。

默认的替换字节数组是单字节0x3F'?'UTF-8 中的符号),因此在编码代码点时就已经得到了它0xD800。根据建议,您可以使用以下命令在较低级别执行此操作CharsetEncoder

static int getUtf8ByteCount(int codePoint) throws CharacterCodingException {
    return StandardCharsets.UTF_8
            .newEncoder()
            .encode(CharBuffer.wrap(new String(new int[] { codePoint }, 0, 1)
                    .toCharArray())).array().length;
}
Run Code Online (Sandbox Code Playgroud)

通过这种方式,0xD800您将获得一个MalformedInputException. 维基百科

孤立的代理代码点没有一般解释

所以基本上你应该决定如何处理这些代码点。返回 3 个字节并不比返回 1 个字节更正确。这只是不正确的输入,因此没有相应的正确输出。

请注意,您的条件按原样if (codePoint > 0x7FFFFFFF)没有意义,因此任何值都不能超过它。可能最好将其替换为0x7FFFFFFFInteger.MAX_VALUEintif (codePoint < 0)