先运行下面代码
public class Test {
public static void main(String[] args) {
char c2 = 'a';
System.out.println("\"a\"字的10进制整数" + (int)c2);
System.out.println("\"a\"字的16进制整数" + Integer.toHexString(c2));
String s2 = "a";
byte[] s2UTF8Bytes = s2.getBytes(StandardCharsets.UTF_8);
System.out.println("\"a\"字的UTF_8编码的字节长度:"+s2UTF8Bytes.length);
for (int i = 0; i < s2UTF8Bytes.length; i++) {
System.out.println("\"a\"字的UTF_8编码的第"+i+"字节为:"+Integer.toHexString(s2UTF8Bytes[i] & 0xFF));
}
char c1 = '丁';
System.out.println("\"丁\"字的10进制整数" + (int)c1);
System.out.println("\"丁\"字的16进制整数" + Integer.toHexString(c1));
String s1 = "丁";
byte[] s1UTF8Bytes = s1.getBytes(StandardCharsets.UTF_8);
System.out.println("\"丁\"字的UTF_8编码的字节长度:"+s1UTF8Bytes.length);
for (int i = 0; i < s1UTF8Bytes.length; i++) {
System.out.println("\"丁\"字的UTF_8编码的第"+i+"字节为:"+Integer.toHexString(s1UTF8Bytes[i] & 0xFF));
}
}
}
运行结果
"a"字的10进制整数97
"a"字的16进制整数61
"a"字的UTF_8编码的字节长度:1
"a"字的UTF_8编码的第0字节为:61
"丁"字的10进制整数19969
"丁"字的16进制整数4e01
"丁"字的UTF_8编码的字节长度:3
"丁"字的UTF_8编码的第0字节为:e4
"丁"字的UTF_8编码的第1字节为:b8
"丁"字的UTF_8编码的第2字节为:81
由运行结果引出几个问题:
- a和丁是如何转为数字的?
- a和丁的数字97、61、19969、4e01、e4b881,他们是如何对应的?
- a的UTF8编码为1字节、丁的UTF8为3字节,如何存入容量是2字节的char类型里面的?
第一个问题很好回答,因为无论是中文字符还是英文字符,甚至其他的生僻字符,都是是以二进制形式存在计算机内存中的,像是01100001这样的,对应的就是十进制的97。
第二个问题,这些文字字符和数字不是随机对应的,是有固定的对应关系的,在java中使用的这个对应关系就是Unicode字符集。
- Unicode是一个字符集,它为世界上几乎所有的字符分配了一个唯一的数值,即码点(code point)。
- Unicode的目标是提供一个全球通用的字符编码,以涵盖世界上所有的文字系统。
- Unicode的码点通常用十六进制表示,例如,中文字符“啊”在Unicode中的码点是U+554A。
查询Unicode码表,
a对应的Unicode编号为U+0061,这是十六进制的,换算为十进制就是97。
丁对应的Unicode编号为U+4E01,十进制为19969
到此,a对应的十进制97、十六进制61 和 丁对应的十进制、十六进制4e01都有了解释,在来看UTF8编码。
UTF-8(8位元,Universal Character Set/Unicode Transformation Format)是针对Unicode的一种可变长度字符编码。它可以用来表示Unicode标准中的任何字符,而且其编码中的第一个字节仍与ASCII相容,使得原来处理ASCII字符的软件无须或只进行少部分修改后,便可继续使用。因此,它逐渐成为电子邮件、网页及其他存储或传送文字的应用中,优先采用的编码。Unicode只是一组字符设定或者说是从数字和字符之间的逻辑映射的概念,但是它并没有指定代码点如何在计算机上存储,Unicode的编码有:UTF-8、UTF-16、UTF-32,UTF后的数字代表编码的最小单位,如UTF-8表示最小单位是8位,即1字节。
下面是UTF-8的编码规则
字节 | 格式 | 实际编码位 | 码点范围 |
---|---|---|---|
1字节 | 0xxxxxxx | 7 | 0 ~ 127 |
2字节 | 110xxxxx 10xxxxxx | 11 | 128 ~ 2047 |
3字节 | 1110xxxx 10xxxxxx 10xxxxxx | 16 | 2048 ~ 65535 |
4字节 | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx | 21 | 65536 ~ 2097151 |
表格中的x就是用unicode对应的二进制进行替换填充。
a的unicode十进制是97,对应码点范围是0~127,只有1字节,
UTF-8的二进制为01100001,十六进制就还是61
丁的unicode十进制是19969,对应码点范围是2048~65535,有3字节,
UTF-8的二进制为11100100 10111000 10000001,十六进制就是 E4 B8 81
其实在前面提到的Unicode码表中已经给出了对应的编码
最后一个问题,char如何存进3字节的中文的?
答案是char存的是UTF-16BE的编码,这个编码的中文是2字节的,见上图。
Unicode是有超过2字节的码点的,这些码点都无法装入java的char类型中,如:
char cc = 😀;
上面的代码是会报错的,
补充:
前面的代码中有这样一段:
Integer.toHexString(s2UTF8Bytes[i] & 0xFF));
为什么要对字符串转为的字节做与运算?
这是因为内存中的二进制是以补码的形式存在的,而Integer.toBinaryString 和 Integer.toHexString 方法都是直接把内存中二进制的给出来,丁的UTF8二进制:11100100 10111000 10000001,系统都会认为是补码,再由byte(11100100 )转为int(11111111111111111111111111100100),8位填充到32位的时候,负数的高位都是补1,结果就是等于了-28,所以需要通过 & 0xFF 运算消除高位多余的1,转为int(00000000000000000000000011100100),对应十进制228 和 十六进制E4,才能获得原本二进制对应的数字。