为什么带有 UTF-8 的新字符串包含更多字节

Why new String with UTF-8 contains more bytes

byte bytes[] = new byte[16];
random.nextBytes(bytes);
try {
   return new String(bytes, "UTF-8");
} catch (UnsupportedEncodingException e) {
   log.warn("Hash generation failed", e);
}

当我使用给定方法生成字符串时,当我应用 string.getBytes().length 它时 returns 一些其他值。最大值为 32。为什么一个 16 字节的数组最终会生成另一个大小的字节字符串?

但如果我这样做 string.length() 它 returns 16.

生成的字节可能包含有效的多字节字符。

以此为例。该字符串仅包含一个字符,但作为字节表示它需要三个字节。

String s = "Ω";
System.out.println("length = " + s.length());
System.out.println("bytes = " + Arrays.toString(s.getBytes("UTF-8")));

String.length() return 字符串的字符长度。字符 是一个字符,而在 UTF-8 中它是 3 个字节长。

如果您像这样更改代码

Random random = new Random();
byte bytes[] = new byte[16];
random.nextBytes(bytes);
System.out.println("string = " + new String(bytes, "UTF-8").length());
System.out.println("string = " + new String(bytes, "ISO-8859-1").length());

用不同的字符集解释相同的字节。并遵循 String(byte[] b, String charset)

中的 javadoc
The length of the new String is a function of the charset, and hence may
not be equal to the length of the byte array.

这是因为您的 bytes 首先被转换为 Unicode 字符串,它试图从这些字节创建 UTF-8 char 序列。如果一个字节不能被视为 ASCII 字符,也不能与下一个字节一起捕获以形成合法的 unicode 字符,则将其替换为“�”。在调用 String#getBytes() 时,此类 char 被转换为 3 个字节,从而向结果输出添加 2 个额外字节。

如果您幸运地只生成 ASCII 字符,String#getBytes() 将 return 16 字节数组,否则,生成的数组可能会更长。例如下面的代码片段:

byte[] b = new byte[16]; 
Arrays.fill(b, (byte) 190);  
b = new String(b, "UTF-8").getBytes(); 

returns 长度为 48(!) 字节的数组。

String.getBytes().length 可能更长,因为它计算表示字符串所需的字节数,而 length() 计算 2 字节代码单元.

阅读更多here

因误解bytes和chars之间的关系而产生的经典错误,所以我们再来一次。

bytechar之间没有一对一的映射;这完全取决于您使用的字符编码(在Java中,即Charset)。

更糟:给定一个 byte 序列,它可能 也可能不会 编码为 char 序列。

例如试试这个:

final byte[] buf = new byte[16];
new Random().nextBytes(buf);

final Charset utf8 = StandardCharsets.UTF_8;
final CharsetDecoder decoder = utf8.newDecoder()
    .onMalformedInput(CodingErrorAction.REPORT);

decoder.decode(ByteBuffer.wrap(buf));

这很有可能抛出一个MalformedInputException

我知道这不是一个确切的答案,但你没有清楚地解释你的问题;上面的例子已经表明你对 byte 是什么和 char 是什么有错误的理解。

这将尝试创建一个字符串,假设字节是 UTF-8。

new String(bytes, "UTF-8");

这通常会出错,因为 UTF-8 多字节序列可能无效。

喜欢:

String s = new String(new byte[] { -128 }, StandardCharsets.UTF_8);

第二步:

byte[] bytes = s.getBytes();

将使用平台编码 (System.getProperty("file.encoding"))。最好具体点。

byte[] bytes = s.getBytes(StandardCharsets.UTF_8);

应该意识到,String 内部会维护 Unicode,一个 16 位 char 的 UTF-16 数组。

人们应该完全避免使用 String 作为 byte[]。它总是涉及转换,占用双倍内存并且容易出错。

如果您查看生成的字符串,就会发现生成的大部分随机字节都不是有效的 UTF-8 字符。因此,String 构造函数将它们替换为 unicode 'REPLACEMENT CHARACTER' �,它占用 3 个字节,0xFFFD。

举个例子:

public static void main(String[] args) throws UnsupportedEncodingException
{
    Random random = new Random();

    byte bytes[] = new byte[16];
    random.nextBytes(bytes);
    printBytes(bytes);

    final String s = new String(bytes, "UTF-8");
    System.out.println(s);
    printCharacters(s);
}

private static void printBytes(byte[] bytes)
{
    for (byte aByte : bytes)
    {
        System.out.print(
                Integer.toHexString(Byte.toUnsignedInt(aByte)) + " ");
    }
    System.out.println();
}

private static void printCharacters(String s)
{
    s.codePoints().forEach(i -> System.out.println(Character.getName(i)));
}

在给定的 运行 上,我得到了这个输出:

30 41 9b ff 32 f5 38 ec ef 16 23 4a 54 26 cd 8c 
0A��2�8��#JT&͌
DIGIT ZERO
LATIN CAPITAL LETTER A
REPLACEMENT CHARACTER
REPLACEMENT CHARACTER
DIGIT TWO
REPLACEMENT CHARACTER
DIGIT EIGHT
REPLACEMENT CHARACTER
REPLACEMENT CHARACTER
SYNCHRONOUS IDLE
NUMBER SIGN
LATIN CAPITAL LETTER J
LATIN CAPITAL LETTER T
AMPERSAND
COMBINING ALMOST EQUAL TO ABOVE