【问题标题】:Calculating length in UTF-8 of Java String without actually encoding it在不实际编码的情况下计算 Java 字符串的 UTF-8 长度
【发布时间】:2012-01-20 15:23:27
【问题描述】:

有谁知道标准 Java 库(任何版本)是否提供了一种计算字符串二进制编码长度(在这种情况下特别是 UTF-8)长度而不实际生成编码输出的方法?换句话说,我正在寻找一个有效的等价物:

"some really long string".getBytes("UTF-8").length

我需要为可能很长的序列化消息计算长度前缀。

【问题讨论】:

  • 如果您的问题是原始速度,而不是内存,您确定 ad-hoc 函数会比 getBytes + length 更快吗?当前的 JRE 在本机代码中实现了相当快的转换例程。
  • 我也担心内存压力,但主要担心的是大分配可能会导致更多的垃圾回收开销。与其引入潜在的性能问题(当然,这需要通过分析来验证),我想我会问是否有更具体的 API 可用。顺便说一句,Oracle 的 JRE 不为此使用本机代码:它们分配一个最坏情况的字节数组(maxBytesPerChar)并使用基于数组的 CharsetEncoder 实现(请参阅sun.nio.cs.UTF_8)。

标签: java utf-8


【解决方案1】:

这是一个基于UTF-8 specification的实现:

public class Utf8LenCounter {
  public static int length(CharSequence sequence) {
    int count = 0;
    for (int i = 0, len = sequence.length(); i < len; i++) {
      char ch = sequence.charAt(i);
      if (ch <= 0x7F) {
        count++;
      } else if (ch <= 0x7FF) {
        count += 2;
      } else if (Character.isHighSurrogate(ch)) {
        count += 4;
        ++i;
      } else {
        count += 3;
      }
    }
    return count;
  }
}

此实现不能容忍格式错误的字符串。

这是一个用于验证的 JUnit 4 测试:

public class LenCounterTest {
  @Test public void testUtf8Len() {
    Charset utf8 = Charset.forName("UTF-8");
    AllCodepointsIterator iterator = new AllCodepointsIterator();
    while (iterator.hasNext()) {
      String test = new String(Character.toChars(iterator.next()));
      Assert.assertEquals(test.getBytes(utf8).length,
                          Utf8LenCounter.length(test));
    }
  }

  private static class AllCodepointsIterator {
    private static final int MAX = 0x10FFFF; //see http://unicode.org/glossary/
    private static final int SURROGATE_FIRST = 0xD800;
    private static final int SURROGATE_LAST = 0xDFFF;
    private int codepoint = 0;
    public boolean hasNext() { return codepoint < MAX; }
    public int next() {
      int ret = codepoint;
      codepoint = next(codepoint);
      return ret;
    }
    private int next(int codepoint) {
      while (codepoint++ < MAX) {
        if (codepoint == SURROGATE_FIRST) { codepoint = SURROGATE_LAST + 1; }
        if (!Character.isDefined(codepoint)) { continue; }
        return codepoint;
      }
      return MAX;
    }
  }
}

请原谅紧凑的格式。

【讨论】:

  • 应该可以,但它是不必要的复杂:您不需要支持 5 和 6 字节字符(因为 Unicode 不允许,并且 UTF-16 不能表示,代码点高),如果Character.isHighSurrogate(ch),那么您实际上不需要确定代码点:UTF-16 中需要代理对的代码点集与 UTF-8 中需要四个字节的代码点集相同。因此,如果没问题。不支持无效的代理对,那么你可以写
  • if(ch &lt;= '\x7F') ++count; else if(ch &lt;= '\u07FF') count += 2; else if(Character.isHighSurrogate(ch)) { count += 4; ++i; } else count += 3;。但是 +1 包括一个超级全面的单元测试。 :-)
  • @ruakh - 所有的优点;我已经用你的实现更新了答案。
  • @McDowell,我认为您的单元测试代码中有错误。当前代码不包括 MAX,但 MAX 是有效的代码点,不是吗?
  • 当使用简单的for 循环而不是AllCodepointsIterator 时,测试用例可能会更加紧凑。此外,java.lang.Character 中已经有方便的常量:for(int codepoint=Character.MIN_CODE_POINT; codepoint&lt;=Character.MAX_CODE_POINT; codepoint++) { if(codepoint == Character.MIN_SURROGATE) codepoint=Character.MAX_SURROGATE+1; if(!Character.isDefined(codepoint)) continue; String test = new String(Character.toChars(codepoint)); Assert.assertEquals(test.getBytes(utf8).length, Utf8LenCounter.length(test)); }
【解决方案2】:

使用 Guava 的Utf8

Utf8.encodedLength("some really long string")

【讨论】:

  • 在 Guava 16.0 中添加,于 2014 年初发布。
【解决方案3】:

我能想到的最好方法是使用 CharsetEncoder 重复写入同一个临时缓冲区:

public int getEncodedLength(CharBuffer src, CharsetEncoder encoder)
    throws CharacterCodingException
{
    // CharsetEncoder.flush fails if encode is not called with >0 chars
    if (!src.hasRemaining())
        return 0;

    // encode into a byte buffer that is repeatedly overwritten
    final ByteBuffer outputBuffer = ByteBuffer.allocate(1024);

    // encoding loop
    int bytes = 0;
    CoderResult status;
    do
    {
        status = encoder.encode(src, outputBuffer, true);
        if (status.isError())
            status.throwException();
        bytes += outputBuffer.position();

        outputBuffer.clear();
    }
    while (status.isOverflow());

    // flush any remaining buffered state
    status = encoder.flush(outputBuffer);
    if (status.isError() || status.isOverflow())
        status.throwException();
    bytes += outputBuffer.position();

    return bytes;
}

public int getUtf8Length(String str) throws CharacterCodingException
{
    return getEncodedLength(CharBuffer.wrap(str),
        Charset.forName("UTF-8").newEncoder());
}

【讨论】:

    【解决方案4】:

    你可以通过字符串循环:

    /**
     * Deprecated: doesn't support surrogate characters.
     */
    @Deprecated
    public int countUTF8Length(String str)
    {
        int count = 0;
        for (int i = 0; i < str.length(); ++i)
        {
            char c = str.charAt(i);
            if (c < 0x80)
            {
                count++;
            } else if (c < 0x800)
            {
                count +=2;
            } else
                throw new UnsupportedOperationException("not implemented yet");
            }
        }
        return count;
    }
    

    【讨论】:

    • 关闭,但不完全是:您没有正确处理代理字符。特别是,c &lt; 0x10000(这是您在编写 c &lt; 0x1000 时的意思)保证为真,因为 BMP 之外的代码点表示为 两个 Java 字符(使用代理代码-点)。
    • @ruakh:啊,是的,我明白了。确实。那是正确的。我想快速解决,但代理字符确实是一个完全正确的问题......但如果它超出了他的需求,这将满足。
    • 这将为代理对返回 8,这是不正确的。由于这些微妙的原因,我试图避免自己编写这段代码。
    • 在 8-16 之间来回组合字符与预组合字符是否有保证? (编码器实现观察到了吗?)我会怀疑是否相信编码器不会生成最终输出的东西。
    • @Affe:我不知道有这样的保证,但我发现真的很难相信有人会编写一个隐式(并且默默地)修改字符序列。我的意思是,这意味着对序列进行编码然后解码会产生一个新的字符串,它不是equals-相当于旧字符串!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-05-03
    • 2022-01-15
    • 2012-07-24
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多