Unicode编码
最近在学习Java的过程中,学到String类时,String类的charAt(index)方法让我产生了很大
的疑问。因此去查阅了unicode编码的相关知识整理如下。
以下内容参考百度百科,整理到这里作为自己的学习笔记。
编码发展史
说到Unicode编码,我们就不得说一说编码的发展史。首先,让我们深层次的了解一下Unicode编码的由来,了解它到底为我们的编码问题解决了什么问题,做出了什么改进。这对我们后面理解Unicode编码有很大的帮助。
ASCII
ASCII码的产生
在计算机中,所有的数据在存储和运算时都要使用二进制数表示,每一个二进制位有0和1两种状态。例如,像a,b,c这样的字母,以及0,1等数字还有一些常用的符号(例如@,*,#等)在计算机中存储时也要使用二进制数来表示。而具体用哪些二进制数表示哪个符号,每个人都可以预定自己的一套规则,这就叫编码。为了能够相互之间进行通信,而又不造成混乱,那么大家就必须使用相同的编码规则。于是美国有关的标准化组织就出台了ASCII编码,统一规定了上述常用符号用哪些二进制数来表示。
表述方式
在计算机中,1字节对应8位二进制数,最初的ASCII码仅用一个字节进行编码。标准ASCII码定义了128个字符,这128个字符只使用了8位2进制数中的后面七位,
最前面的一位统一规定为0,后用作奇偶校验位。
这里就顺便的介绍一下什么是奇偶校验位:
所谓奇偶校验,是指在代码传送过程中用来检验是否出现错误的一种方法,一般分奇校验和偶校验两种。奇校验规定:正确的代码一个字节中1的个数必须是奇数,若非奇数,则在最高位添1;偶校验规定:正确的代码一个字节中1的个数必须是偶数,若非偶数,则在最高位添1。
ASCII码存在的问题
ASCII是美国标准,他只实现了他们需要用到的字符。所以他不能良好满足其他讲英语国家的需要。例如英国的英镑符号(£)在哪里?更不用说中国汉字,拉丁语等字符了。1967年,国际标准化组织推荐了一个ASCII的变种,代码0x40、0x5B、0x5C、0x5D、0x7B、0x7C和0x7D"为国家使用保留",而代码0x5E、0x60和0x7E标为"当国内要求的特殊字符需要8、9或10个空间位置时,可用于其它图形符号"。他的意思就是在0-127号字符上我们达成一致,但是对于128-255号字符可以根据自己国家的需要进行编码。但是这又产生了新的问题,每一个国家的编码方式有可能不同,那么在国家通信之间就会发生很大的问题,这不是我们想要看到的。后来,美国又提出了内码表的概念,每一个编码方式对应一个内码表,想要显示相应语言的字符时,只需要切换到相应语言的内码表就可以了。但是在国家通信中,我们就不得不频繁在内码表内进行切换,这显然是很麻烦的。
Unicode
为了保证一致性,而又不想频繁的切换内码表,那就需要把世界上所有语言中的所有字符都整合起来。因此,Unicode诞生了。
Unicode编码方式
在这里我们先了解几个概念
- 码点:码点是指一个编码表中的某个字符对应的代码值。
- 代码平面:Unicode字符分为17组编排,0x0000至0x10FFFF,每组称为代码平面。
- 基本多语言平面:第一个代码平面称为基本多语言平面,包括码点从U+0000到U+FFFF的“经典”Unicode代码。
在1991年发布了unicode 1.0,当时仅占用65536个代码值中不到一半的部分。在设计Java时决定采用16位的unicode字符集。但是经过一段时间后,不可避免的事情发生了。unicode字符超过了65536个,其主要原因是增加了大量的汉语,日语和韩语中的表意文字。现在,16位二进制数已经不能满足描述所有unicode字符的需要了。
现在,在计算机里,有只需要1个字节编码的字符,也有2个,3个,甚至4个字节编码的字符。那么计算机如何识别呢?
于是,UTF-8 和 UTF-16 两种当前比较流行的编码方式诞生了。
UTF-8
UTF-8以字节为单位对Unicode进行编码。从Unicode到UTF-8的编码方式如下:
Unicode编码(16进制) | UTF-8字节流(二进制) |
---|---|
000000-00007F | 0xxxxxxx |
0000080-0007FF | 110xxxxx 10xxxxxx |
000800-00FFFF | 1110xxxx 10xxxxxx 10xxxxxx |
010000-10FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
UTF-8的特点是对不同范围的字符使用不同长度的编码。对于0x00-0x7F之间的字符,UTF-8编码与ASCII编码完全相同。UTF-8编码的最大长度是4个字节。从上表可以看出,4字节模板有21个x,即可以容纳21位二进制数字。Unicode的最大码位0x10FFFF也只有21位。
例1:"汉"字的Unicode编码是0x6C49。0x6C49在0x0800-0xFFFF之间,使用3字节模板:1110xxxx 10xxxxxx 10xxxxxx。将0x6C49写成二进制是:0110 1100 0100 1001, 用这个比特流依次代替模板中的x,得到:11100110 10110001 10001001,即E6 B1 89。
例2:Unicode编码0x20C30在0x010000-0x10FFFF之间,使用4字节模板:11110xxx 10xxxxxx 10xxxxxx 10xxxxxx。将0x20C30写成21位二进制数字(不足21位就在前面补0):0 0010 0000 1100 0011 0000,用这个比特流依次代替模板中的x,得到:11110000 10100000 10110000 10110000,即F0 A0 B0 B0。
UTF-16
UTF-16编码以16位无符号整数为单位。我们把Unicode编码记作U。编码规则如下:
- 如果U<0x10000,则U的UTF-16编码就是U对应的16位无符号整数
- 如果U≥0x10000,我们先计算U’=U-0x10000,然后将U’写成二进制形式:yyyy yyyy yyxx xxxx xxxx,U的UTF-16编码(二进制)就是:110110yyyyyyyyyy 110111xxxxxxxxxx。
为什么U’可以被写成20个二进制位?Unicode的最大码位是0x10FFFF,减去0x10000后,U’的最大值是0xFFFFF,所以肯定可以用20个二进制位表示。
例如:Unicode编码0x20C30,减去0x10000后,得到0x10C30,写成二进制是:0001 0000 1100 0011 0000。用前10位依次替代模板中的y,用后10位依次替代模板中的x,就得到:1101100001000011 1101110000110000,即0xD843 0xDC30。
按照上述规则,Unicode编码0x10000-0x10FFFF的UTF-16编码有两个16位无符号整数,第一个16位无符号整数的高6位是110110,第二个16位无符号整数的高6位是110111。可见,第一个16位无符号整数的取值范围(二进制)是11011000 00000000到11011011 11111111,即0xD800-0xDBFF。第二个16位无符号整数的取值范围(二进制)是11011100 00000000到11011111 11111111,即0xDC00-0xDFFF。
为了将一个16位无符号整数的UTF-16编码与两个16位无符号整数的UTF-16编码区分开来,Unicode编码的设计者将0xD800-0xDFFF保留下来,并称为代理区:
D800-DB7F | High Surrogates | 高位替代 |
DB80-DBFF | High Private Use Surrogates | 高位专用替代 |
DC00-DFFF | Low Surrogates | 低位替代 |
高位替代就是指这个范围的码位是两个16位无符号整数的UTF-16编码的第一个16位无符号整数。低位替代就是指这个范围的码位是两个16位无符号整数的UTF-16编码的第二个16位无符号整数。那么,高位专用替代是什么意思?我们来解答这个问题,顺便看看怎么由UTF-16编码推导Unicode编码。
如果一个字符的UTF-16编码的第一个16位无符号整数在0xDB80到0xDBFF之间,那么它的Unicode编码在什么范围内?我们知道第二个16位无符号整数的取值范围是0xDC00-0xDFFF,所以这个字符的UTF-16编码范围应该是0xDB80 0xDC00到0xDBFF 0xDFFF。我们将这个范围写成二进制:
1101101110000000 11011100 00000000 - 1101101111111111 1101111111111111
按照编码的相反步骤,取出高低16位无符号整数的后10位,并拼在一起,得到
1110 0000 0000 0000 0000 - 1111 1111 1111 1111 1111即0xE0000-0xFFFFF,按照编码的相反步骤再加上0x10000,得到0xF0000-0x10FFFF。这就是UTF-16编码的第一个16位无符号整数在0xdDB0到0xDBFF之间的Unicode编码范围,即平面15和平面16。因为Unicode标准将平面15和平面16都作为专用区,所以0xDB80到0xDBFF之间的保留码位被称作高位专用替代。
我的问题
String类的charAt(index)方法将返回位置index的代码单元
但是类似🍺的字符需要两个代码单元
当我们使用"🍺".charAt(0)时,返回的并不是🍺字符,而是🍺字符的第一个代码单元。
public class UnicodeTest {
public static void main(String[] args) {
String s = "🍺";
char str1 = s.charAt(0);//返回🍺的第一个代码单元
char str2 =s.charAt(1);//返回🍺的第二个代码单元
System.out.println(str1);//?
System.out.println("\ud83c");//等同于System.out.println(str1);
System.out.println(str2);//?
System.out.println("\udf7a");//等同于System.out.println(str2);
new UnicodeTest().toBinary(s);//调用toBinary方法
System.out.println("\ud83c"+"\udf7a");//输出🍺
}
public void toBinary(String str){//将字符串str转换为二进制数输出
char[] strChar=str.toCharArray();//将字符串str存入数组char[]
String result="";//存放结果
for(int i=0;i<strChar.length;i++){
result +=Integer.toBinaryString(strChar[i])+ " ";//转换二进制写入结果
}
System.out.println(result);
}
}
运行代码如下: