想探讨Java中字符串的UTF-8编码格式,首先想到的是直接将字符串的字节码打印输出。例如,以下代码试图打印中文”赵”字的UTF-8编码的字节数组。
public class Utf8 {
public static void main(String[] args) {
byte[] bts = "赵".getBytes(StandardCharsets.UTF_8);
System.out.println(Arrays.toString(bts));
}
}
输出结果如下:
[-24, -75, -75]
输出结果是3个负数,如何理解这个输出结果?
首先,我们知道在Java中,字符串都是使用Unicode(通用字符集)进行编码的。“赵”字在Unicode中的code point(位点)为\u8d75。而本例中UTF-8(8-bit Unicode Transformation Format)是Unicode最常用的一种编码格式。
UTF-8的编码格式规则如下:
代码范围(16进制) | 二进制编码格式 | 备注 |
000000 – 00007f | 0xxxxxxx | ASCII字符范围,以0开始 |
000080 – 0007ff | 110xxxxx 10xxxxxx | 第1个字节110开始, 第2个字节10开始 |
000800 – 00D7ff 00e000 – 00ffff | 1110xxxx 10xxxxxx 10xxxxxx | 第1个字节1110开始,其他字节10开始 |
010000 – 10ffff | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx | 第1个字节11110开始,其他字节10开始 |
编码规则可以看出,UTF-8是一种可变长度字符编码,也是一种前缀码。“赵”的位点16进制8d75,属于3字节的编码规则,所以UTF-8编码格式应该为
1110xxxx 10xxxxxx 10xxxxxx
8d75的二进制表示为
1000 1101 0111 0101
则“赵”的UTF-8编码二进制表示为
11101000 10110101 10110101
Java中对字节进行打印,其实就是将二进制表示的十进制真值输出。而Java中实际存储的是二进制的补码,正数的补码即真值,而负数的补码减1然后取反码即为真值的二进制表示。
因为3个字节第1位符号位(0表示正数,1表示负数)都为1,所以都为负数。则UTF-8编码字节数组的真值应该表示为
10011000 11001011 11001011
最后进行二进制转十进制转换,后7位为数值的绝对值。所以,打印结果为
[-24, -75, -75]