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

Java: UTF-8 byte length of a single code point (surrogates again)

这一切都是从一个非常基本的问题开始的:给定一个 char —— 或者更确切地说,一个整数代码点,参见 Character API ——,return 所需的字节数因为它的 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;
}

或者喜欢的人:

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

然后我创建了另一个版本(基于UTF-8 wikipedia article)为了简单和可能的效率:

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;
}

经过多年与字符编码的许多可爱的微妙之处斗争之后,我 运行 进行了测试,看!它失败了;对于从 '\uD800' 到 '\uDFFF' 的所有代码点,"stdlib" 版本 returns 1 个字节与 "hand-rolled" 的 3 个字节。可以肯定的是,这是好的 ol' 代理角色再次造成破坏!现在,根据我对那些讨厌的小家伙的理解,我会说第二个版本是正确的。我的问题:

  1. String.getBytes()或(Java的UTF-8实现)坏了,还是我的理解? (我正在使用 Oracle Java SE Runtime Environment 1.6.0_22-b04)
  2. 即使不正确,它是否比 "hand-rolled" 版本更好,因为它与 Java 的 UTF-8 生成的实际字节 encoding/decoding 更一致?
  3. 抛开正确性考虑,Java 标准库是否提供了比我的 "stlib" 更简洁的方法?

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

中描述

This method always replaces malformed-input and unmappable-character sequences with this charset's default replacement byte array. The CharsetEncoder class should be used when more control over the encoding process is required.

默认的替换字节数组是单字节 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;
}

通过这种方式提供 0xD800 你将得到一个 MalformedInputException。维基百科 says:

Isolated surrogate code points have no general interpretation

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

请注意,您的 if (codePoint > 0x7FFFFFFF) 条件没有意义,因为 0x7FFFFFFFInteger.MAX_VALUE,因此任何 int 值都不能超过它。可能最好用 if (codePoint < 0)

替换它