【问题标题】:Get size of String w/ encoding in bytes without converting to byte[]在不转换为字节 [] 的情况下获取带编码的字符串大小(以字节为单位)
【发布时间】:2013-11-08 05:49:00
【问题描述】:

我有一种情况,我需要知道String/编码对的大小(以字节为单位),但不能使用getBytes() 方法,因为1)String 非常大并且复制了Stringbyte[] 数组中将使用大量内存,但更重要的是 2) getBytes() 根据 String 的长度分配 byte[] 数组 * 每个字符的最大可能字节数。因此,如果我有一个带有 1.5B 字符和 UTF-16 编码的 StringgetBytes() 将尝试分配一个 3GB 数组并失败,因为数组限制为 2^32 - X 字节(X 是特定于 Java 版本的)。

那么 - 有没有办法直接从 String 对象计算 String/编码对的字节大小?

更新:

这是 jtahlborn 回答的一个有效实现:

private class CountingOutputStream extends OutputStream {
    int total;

    @Override
    public void write(int i) {
        throw new RuntimeException("don't use");
    }
    @Override
    public void write(byte[] b) {
        total += b.length;
    }

    @Override public void write(byte[] b, int offset, int len) {
        total += len;
    }
}

【问题讨论】:

  • 字节长度取决于您的目标编码。例如,“test”.getBytes("UTF-8") 是 4 个字节,但 "test".getBytes("UTF-16") 是 10 个字节(是的,10 个,试试看)。所以你需要澄清一下你的问题。
  • 我要补充一点,它还取决于您正在编码的代码点(“字符”)。例如,在 UTF-16 中,某些代码点使用 1 个代码单元,而其他代码点使用 2 个(代码单元为 16 位长)。 UTF-8 每个字符可以占用 1 到 4 个字节。
  • @brettw 对不起,如果我很密集,但是是的,你的评论是问题的关键:给定一个字符串和一个编码,编码字符串需要多少字节?重读这个问题,这对我来说似乎很清楚 - 你对改写它有什么建议吗?
  • @Francis 上面的评论也适用于您的评论,尽我所能。
  • getByte 不会创建一个比它需要的更大的数组。它为给定的字符串创建一个正确大小的数组。它不会创建长度为“字符串长度 * 每个字符的最大可能字节数”的数组。而string.length() 不返回字符串中的字符数,它返回代码单元的数量。对于 UTF-16,一个代码单元是 16 位,每个字符的代码单元数是 1 或 2,这取决于字符。因此,要么我不明白你问题中的第二点,要么你的假设不正确。

标签: java string size byte


【解决方案1】:

简单,只需将其写入虚拟输出流:

class CountingOutputStream extends OutputStream {
  private int _total;

  @Override public void write(int b) {
    ++_total;
  }

  @Override public void write(byte[] b) {
    _total += b.length;
  }

  @Override public void write(byte[] b, int offset, int len) {
    _total += len;
  }

  public int getTotalSize(){
     _total;
  }
}

CountingOutputStream cos = new CountingOutputStream();
Writer writer = new OutputStreamWriter(cos, "my_encoding");
//writer.write(myString);

// UPDATE: OutputStreamWriter does a simple copy of the _entire_ input string, to avoid that use:
for(int i = 0; i < myString.length(); i+=8096) {
  int end = Math.min(myString.length(), i+8096);
  writer.write(myString, i, end - i);
}

writer.flush();

System.out.println("Total bytes: " + cos.getTotalSize());

它不仅简单,而且可能与其他“复杂”答案一样快。

【讨论】:

  • @elhefe - 您的版本可以编译,但不正确。您不想在计算中使用偏移量。
  • 糟糕,已修复。显然我的测试只使用了 write(byte[]) 方法。
  • @AminSuzani - 将 _total 更改为 long 就足够了。
  • 我不确定这里保存的是什么。在尝试进行字节转换之前,字符串仍将被 OutputStreamWriter 复制到一个 char 数组中(通过 StreamEncoder.write((String str, int off, int len) 方法)。
  • 但这并不能解决 OP 的问题。您只是将 OP 试图摆脱的 byte[] 数组分配替换为另一个(char[] 数组),它可能会变成相同的大小。当然,如果我有解决方案,我会发布它:)。
【解决方案2】:

同样使用 apache-commons 库:

public static long stringLength(String string, Charset charset) {

    try (NullOutputStream nul = new NullOutputStream();
         CountingOutputStream count = new CountingOutputStream(nul)) {

        IOUtils.write(string, count, charset.name());
        count.flush();
        return count.getCount();
    } catch (IOException e) {
        throw new IllegalStateException("Unexpected I/O.", e);
    }
}

【讨论】:

    【解决方案3】:

    这是一个明显有效的实现:

    import java.nio.charset.Charset;
    import java.nio.charset.StandardCharsets;
    
    public class TestUnicode {
    
        private final static int ENCODE_CHUNK = 100;
    
        public static long bytesRequiredToEncode(final String s,
                final Charset encoding) {
            long count = 0;
            for (int i = 0; i < s.length(); ) {
                int end = i + ENCODE_CHUNK;
                if (end >= s.length()) {
                    end = s.length();
                } else if (Character.isHighSurrogate(s.charAt(end))) {
                    end++;
                }
                count += encoding.encode(s.substring(i, end)).remaining() + 1;
                i = end;
            }
            return count;
        }
    
        public static void main(String[] args) {
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < 100; i++) {
                sb.appendCodePoint(11614);
                sb.appendCodePoint(1061122);
                sb.appendCodePoint(2065);
                sb.appendCodePoint(1064124);
            }
            Charset cs = StandardCharsets.UTF_8;
    
            System.out.println(bytesRequiredToEncode(new String(sb), cs));
            System.out.println(new String(sb).getBytes(cs).length);
        }
    }
    

    输出是:

    1400
    1400
    

    实际上,我会将ENCODE_CHUNK 增加到 10MChars 左右。

    可能比 brettw 的答案效率略低,但实现起来更简单。

    【讨论】:

    • 这还不错,考虑到其他解决方案的OutputStreamWriter 也会在将缓冲区传递给CountingOutputStream 之前对缓冲区执行实际编码操作。唯一的缺点是您的解决方案分配了新的ByteBuffer 实例。当您通过实施standard encoding loop 解决此问题时,您将获得最快的(通用)解决方案。请参阅this answer 了解专门针对 UTF-8 的廉价计算。
    【解决方案4】:

    Guava 有一个根据这个post 的实现:

    Utf8.encodedLength()

    【讨论】:

      【解决方案5】:

      好吧,这太恶心了。我承认这一点,但是这些东西被 JVM 隐藏了,所以我们必须稍微挖掘一下。还有一点汗水。

      首先,我们需要实际的 char[] 来支持 String 而不制作副本。为此,我们必须使用反射来获取“值”字段:

      char[] chars = null;
      for (Field field : String.class.getDeclaredFields()) {
          if ("value".equals(field.getName())) {
              field.setAccessible(true);
              chars = (char[]) field.get(string); // <--- got it!
              break;
          }
      }
      

      接下来您需要实现java.nio.ByteBuffer 的子类。比如:

      class MyByteBuffer extends ByteBuffer {
          int length;            
          // Your implementation here
      };
      

      忽略所有 getter,实现所有 put 方法,例如 put(byte)putChar(char) 等。在 put(byte) 之类的内部,递增 length 加 1,在 put(byte[]) 内将 length 增加数组长度。得到它?放置的所有内容,您将其大小添加到 length。但是您没有在ByteBuffer 中存储任何内容,您只是在数数并丢弃,因此不会占用任何空间。如果您对put 方法进行断点,您可能会找出您实际需要实现的那些方法。例如,putFloat(float) 可能没有被使用。

      现在是大结局,把它们放在一起:

      MyByteBuffer bbuf = new MyByteBuffer();         // your "counting" buffer
      CharBuffer cbuf = CharBuffer.wrap(chars);       // wrap your char array
      Charset charset = Charset.forName("UTF-8");     // your charset goes here
      CharsetEncoder encoder = charset.newEncoder();  // make a new encoder
      encoder.encode(cbuf, bbuf, true);               // do it!
      System.out.printf("Length: %d\n", bbuf.length); // pay me US$1,000,000
      

      【讨论】:

      • 您可以避免丑陋的反射,只需调用 CharBuffer.wrap(CharSequence)String 本身。它使用String中的char[]而不进行复制(至少在Oracle JDK 7 Update 21中)。
      • 哦,太好了!我不知道。
      • 正如@JoachimSauer 很久以前所说的那样,不需要这种反射黑客,那么为什么这个答案仍然从它开始呢?从 Java 9 开始,这将失败,因为内部数组不是 char[](更不用说它更早失败的替代 JRE 实现)。除此之外,循环getDeclaredFields() 而不是仅仅调用getDeclaredField("value") 很奇怪,但无论如何。您回答的主要思想是在应用程序中创建ByteBuffer 的子类,这是不可能的。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2011-09-08
      • 2013-02-06
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-06-17
      • 2013-04-26
      相关资源
      最近更新 更多