6

恐怕我对一个相当过饱和的主题的细节有疑问,我搜索了很多,但找不到一个明确的答案来解决这个特定的明显 - 恕我直言 - 重要的问题:

使用 UTF-8 将 byte[] 转换为 String 时,每个字节(8bit)都变成了 UTF-8 编码的 8 位字符,但在 java 中每个 UTF-8 字符都保存为 16 位字符。那是对的吗?如果是,这意味着每个愚蠢的 java 字符只使用前 8 位,并消耗双倍的内存?这也正确吗?我想知道这种浪费行为是如何被接受的。

有一个 8 位的伪字符串没有什么技巧吗?这实际上会导致更少的内存消耗吗?或者,有没有办法在一个java 16位字符中存储>两个< 8位字符以避免这种内存浪费?

感谢您提供任何令人困惑的答案...

编辑:嗨,谢谢大家的回答。我知道 UTF-8 的可变长度属性。但是,由于我的源是 8 位字节,我理解(显然是错误的)它只需要 8 位 UTF-8 字。UTF-8 转换是否实际上保存了您在 CLI 上执行“cat somebinary”时看到的奇怪符号?我认为 UTF-8 只是以某种方式用于将字节的每个可能的 8 位字映射到 UTF-8 的一个特定的 8 位字。错误的?我考虑过使用 Base64 但这很糟糕,因为它只使用 7 位..

重新制定的问题:有没有更聪明的方法将字节转换为字符串?可能最喜欢的是将 byte[] 转换为 char[],但我仍然有 16 位字。

其他用例信息:

我正在改编Jedis(NoSQL Redis 的 java 客户端)作为 hypergraphDB 的“原始存储层”。所以,jedis是另一个“数据库”的数据库。我的问题是我必须一直为 jedis 提供 byte[] 数据,但在内部,>Redis<(实际的服务器)只处理“二进制安全”字符串。由于 Redis 是用 C 编写的,因此 char 是 8 位长,AFAIK 不是 7 位的 ASCIII。然而,在 Jedis 和 Java 世界中,每个字符的内部长度都是 16 位。我还不明白这段代码,但我想 jedis 然后将这个 java 16 位字符串转换为符合 Redis 的 8 位字符串(([这里] [3])。它说它扩展了 FilterOutputStream。我希望绕过byte[] <-> 字符串转换并使用那个Filteroutputstream ...?)

现在我想知道:如果我必须一直相互转换 byte[] 和 String,数据大小从非常小到可能非常大,在 java 中将每个 8 位字符作为 16 位传递是否会浪费大量内存?

4

7 回答 7

9

有一个 8 位的伪字符串没有什么技巧吗?

是的,请确保您拥有最新版本的 Java。;)

http://www.oracle.com/technetwork/java/javase/tech/vmoptions-jsp-140102.html

-XX:+UseCompressedStrings 对可以表示为纯 ASCII 的字符串使用 byte[]。(在 Java 6 Update 21 Performance Release 中引入)

编辑:此选项在 Java 6 更新 22 中不起作用,并且默认情况下在 Java 6 更新 24 中未启用。注意:看来此选项可能会使性能降低约 10%。

以下程序

public static void main(String... args) throws IOException {
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < 10000; i++)
        sb.append(i);

    for (int j = 0; j < 10; j++)
        test(sb, j >= 2);
}

private static void test(StringBuilder sb, boolean print) {
    List<String> strings = new ArrayList<String>();
    forceGC();
    long free = Runtime.getRuntime().freeMemory();

    long size = 0;
    for (int i = 0; i < 100; i++) {
        final String s = "" + sb + i;
        strings.add(s);
        size += s.length();
    }
    forceGC();
    long used = free - Runtime.getRuntime().freeMemory();
    if (print)
        System.out.println("Bytes per character is " + (double) used / size);
}

private static void forceGC() {
    try {
        System.gc();
        Thread.sleep(250);
        System.gc();
        Thread.sleep(250);
    } catch (InterruptedException e) {
        throw new AssertionError(e);
    }
}

默认打印这个

Bytes per character is 2.0013668655941212
Bytes per character is 2.0013668655941212
Bytes per character is 2.0013606946433575
Bytes per character is 2.0013668655941212

有选项-XX:+UseCompressedStrings

Bytes per character is 1.0014671435440285
Bytes per character is 1.0014671435440285
Bytes per character is 1.0014609725932648
Bytes per character is 1.0014671435440285
于 2011-04-12T13:56:12.840 回答
5

实际上,您的 UTF-8 部分是错误的:UTF-8 是一种可变长度的多字节编码,因此有效字符的长度为 1-4 个字节(换句话说,有些 UTF-8 字符是 8 位的,有些是16 位,有些是 24 位,有些是 32 位)。虽然 1 字节字符占用 8 位,但还有更多的多字节字符。如果你只有 1 个字节的字符,那么总共只能有 256 个不同的字符(又名“扩展 ASCII”);这可能足以满足 90% 的英语使用(我的幼稚猜测),但一旦您想到超出该子集的任何内容,就会让您大吃一惊(请参阅单词 naïve - 英语,但不能只写与 ASCII)。

因此,尽管 UTF-16(Java 使用的)看起来很浪费,但实际上并非如此。无论如何,除非你在一个非常有限的嵌入式系统上(在这种情况下,你在用 Java 做什么?),试图减少字符串是毫无意义的微优化。

有关字符编码的稍长介绍,请参见例如:http ://www.joelonsoftware.com/articles/Unicode.html

于 2011-04-12T12:12:29.680 回答
2

使用 UTF-8 将 byte[] 转换为 String 时,每个字节(8 位)变成 UTF-8 编码的 8 位字符

不可以。当转换byte[]String使用 UTF-8 时,每个1-6 字节的UTF-8 序列都将转换为1-2 个 16 位字符的 UTF-16序列。

在全球几乎所有情况下,这个 UTF-16 序列都包含一个字符。

在西欧和北美,对于大多数文本,这个 16 位字符中只有 8 位被使用。但是,如果您有欧元符号,则需要超过 8 位。

有关详细信息,请参阅Unicode。或Joel Spolsky 的文章

于 2011-04-12T12:29:04.863 回答
2

Java 在内部将其所有“字符”存储为值的两个字节表示形式。但是,它们的存储方式与 UTF-8 不同。例如,支持的最大值是“\uFFFF”(十六进制 FFFF,十进制 65536)或 11111111 11111111 二进制(两个字节)——但这将是磁盘上的 3 字节 Unicode 字符。

唯一可能的浪费是内存中真正的“单”字节字符(大多数 ASCII“语言”字符实际上适合 7 位)。当字符写入磁盘时,它们无论如何都会采用指定的编码(因此 UTF-8 单字节字符将只占用一个字节)。

唯一不同的地方是 JVM 堆。但是,您必须拥有成千上万个 8 位字符才能注意到 Java 堆使用的任何真正差异——这将远远超过您所做的所有额外(hacky)处理。

无论如何,RAM 中的一百万个 8 位字符只会“浪费”大约 1 MiB...

于 2011-04-12T13:49:10.697 回答
1

Redis(实际的服务器)只处理“二进制安全”字符串。

我认为这意味着您可以对键/值使用任意八位字节序列。如果您可以使用任何 Cchar序列而不考虑字符编码,那么 Java 中的等价物就是byte类型。

Java 中的字符串是隐式的UTF-16。我的意思是,您可以在其中粘贴任意数字,但该类的目的是表示 Unicode 字符数据。执行转换的方法执行从已知编码到byteUTF -16 的转码操作。char

如果 Jedis 将键/值视为 UTF-8,则它不会支持 Redis 支持的每个值。并非每个字节序列都是有效的 UTF-8,因此编码不能用于二进制安全字符串。


UTF-8 或 UTF-16 是否消耗更多内存取决于数据 - 例如欧元符号 (€) 在 UTF-8 中消耗三个字节,而在 UTF-16 中仅消耗两个字节。

于 2011-04-12T15:00:11.940 回答
0

只是为了记录,我编写了自己的 byte[] <-> String 互转换器的小实现,它通过将每 2 个字节转换为 1 个字符来工作。它大约快 30-40% 并且消耗(可能少于)Java 标准方式的一半内存:new String(somebyte) 和 someString.getBytes()。

但是,它与现有的字符串编码字节或字节编码字符串不兼容。此外,在共享数据上从不同的 JVM 调用该方法是不安全的。

https://github.com/ib84/castriba

于 2011-04-26T14:58:50.793 回答
-1

也许这就是你想要的:

// Store them into the 16 bit datatype.
char c1_8bit = 'a';
char c2_8bit = 'h';
char two_chars = (c1_8bit << 8) + c2_8bit;

// extract them
char c1_8bit = two_chars >> 8;
char c2_8bit = two_chars & 0xFF;

当然,这个技巧只适用于 ASCII 字符([0-255] 范围内的字符)。为什么?因为你想以这种方式存储你的字符:
xxxx xxxx yyyy yyyywith xis char 1 and yis char 2。所以这意味着你每个字符只有 8 位。用 8 位可以得到的最大整数是多少?答案:255

255= 0000 0000 1111 1111(8位)。当您使用 char > 255 时,您将拥有:
256= 0000 0001 0000 0000(超过 8 位),它不适合您为 1 个字符提供的 8 位。

另外:请记住,Java 是一种由聪明人开发的语言。他们知道他们在哪里做什么。推动 Java API

于 2011-04-12T12:12:51.413 回答