Unicode(统一码、万国码、单一码)是计算机科学领域里的一项业界标准,包括字符集、编码方案等。Unicode 是为了解决传统的字符编码方案的局限而产生的,它为每种语言中的每个字符设定了统一并且唯一的二进制编码,以满足跨语言、跨平台进行文本转换、处理的要求。1990年开始研发,1994年正式公布。
ASCII编码
非ASCII编码
unicode编码
UTF-8
UTF-16
JAVA字符编码
ASCII 是用来表示英文字符的一种编码规范。每个ASCII字符占用1 个字节,因此,ASCII编码可以表示的最大字符数是255(00H—FFH)。这对于英文而言,是没有问题的,一般只什么用到前128个(00H–7FH,最高位为0)。而最高位为1 的另128个字符(80H—FFH)被称为“扩展ASCII”,一般用来存放英文的制表符、部分音标字符等等的一些其它符号。
但是对于中文等比较复杂的语言,255个字符显然不够用。于是,各个国家纷纷制定了自己的文字编码规范,其中中文的文字编码规范叫做“GB2312—80”, 它是和ASCII兼容的一种编码规范, 其实就是利用扩展ASCII没有真正标准化这一点,把一个中文字符用两个扩展ASCII字符来表示,以区分ASCII码部分。但是这个方法有问题,最大的问题就是中文的文字编码和扩展ASCII 码有重叠。而很多软件利用扩展ASCII 码的英文制表符来画表格,这样的软件用到中文系统中,这些表格就会被误认作中文字符,出现乱码。另外,由于各国和各地区都有自己的文字编码规则,它们互相冲突,这给各国和各地区交换信息带来了很大的麻烦。
英语用128个符号编码就够了,但是用来表示其他语言,128个符号是不够的。比如,在法语中,字母上方有注音符号,它就无法用ASCII码表示。于是,一些欧洲国家就决定,利用字节中闲置的最高位编入新的符号。比如,法语中的é的编码为130(二进制10000010)。这样一来,这些欧洲国家使用的编码体系,可以表示最多256个符号。
但是,这里又出现了新的问题。不同的国家有不同的字母,因此,哪怕它们都使用256个符号的编码方式,代表的字母却不一样。比如,130在法语编码中代表了é,在希伯来语编码中却代表了字母Gimel (ג),在俄语编码中又会代表另一个符号。但是不管怎样,所有这些编码方式中,0–127表示的符号是一样的,不一样的只是128–255的这一段。
至于亚洲国家的文字,使用的符号就更多了,汉字就多达10万左右。一个字节只能表示256种符号,肯定是不够的,就必须使用多个字节表达一个符号。比如,简体中文常见的编码方式是GB2312,使用两个字节表示一个汉字,所以理论上最多可以表示256x256=65536个符号。
各个国家都像中国这样搞出一套自己的编码标准,结果互相之间谁也不懂谁的编码,谁也不支持别人的编码。当时的中国人想让电脑显示汉字,就必须装上一个”汉字系统”,专门用来处理汉字的显示、输入的问题,装错了字符系统,显示就会乱了套。这怎么办?就在这时,一个叫 ISO (国际标谁化组织)的国际组织决定着手解决这个问题。他们采用的方法很简单:废了所有的地区性编码方案,重新搞一个包括了地球上所有文化、所有字母和符号的编码!他们打算叫它”Universal Multiple-Octet Coded Character Set”,简称 UCS, 俗称 “UNICODE”。
中国人民通过对 ASCII 编码的中文扩充改造,产生了 GB2312 编码,可以表示6000多个常用汉字。
汉字实在是太多了,包括繁体和各种字符,于是产生了 GBK 编码,它包括了 GB2312 中的编码,同时扩充了很多。
中国是个多民族国家,各个民族几乎都有自己独立的语言系统,为了表示那些字符,继续把 GBK 编码扩充为 GB18030 编码。
每个国家都像中国一样,把自己的语言编码,于是出现了各种各样的编码,如果你不安装相应的编码,就无法解释相应编码想表达的内容。
终于,有个叫 ISO 的组织看不下去了。他们一起创造了一种编码 UNICODE ,这种编码非常大,大到可以容纳世界上任何一个文字和标志。所以只要电脑上有 UNICODE 这种编码系统,无论是全球哪种文字,只需要保存文件的时候,保存成 UNICODE 编码就可以被其他电脑正常解释。
UNICODE 在网络传输中,出现了两个标准 UTF-8 和 UTF-16,分别每次传输 8个位和 16个位。
于是就会有人产生疑问,UTF-8 既然能保存那么多文字、符号,为什么国内还有这么多使用 GBK 等编码的人?因为 UTF-8 等编码体积比较大,占电脑空间比较多,如果面向的使用人群绝大部分都是中国人,用 GBK 等编码也可以。但是目前的电脑来看,硬盘都是白菜价,电脑性能也已经足够无视这点性能的消耗了。所以推荐所有的网页使用统一编码:UTF-8。
常用的unicode编码值范围为“0-0xFFFF”
实际unicode编码值范围为“0-0x10FFFF”
应当注意,Unicode只是一个符号集,它只规定了符号的二进制代码,却没有规定这个二进制代码应该如何存储。
有些字符用一个字节就能表示,有些则需要2个或3个字节,甚至有4个字节。如果unicode统一规定,每个符号用三个或四个字节表示,那么每个英文字母势必有二到三个字节是0,这对于存储来说是极大的浪费。
所以unicode很长一段时间内无法推广。
互联网的普及,强烈要求出现统一的编码方式。UTF-8就是在互联网上使用最广的一种Unicode的实现方式。其他实现方式还包括UTF-16(字符用两个字节或四个字节表示)和UTF-32(字符用四个字节表示),不过在互联网上基本不用。
UTF-8是Unicode的实现方式之一。
UTF-8最大的一个特点,就是它是一种变长的编码方式。它可以使用1~4个字节表示一个符号,根据不同的符号而变化字节长度。
UTF-8的编码规则很简单,只有二条:
对于单字节的符号,字节的第一位设为0,后面7位为这个符号的unicode码。因此对于英语字母,UTF-8编码和ASCII码是相同的。
对于n字节的符号(n>1),第一个字节的前n位都设为1,第n+1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的unicode码。
unicode符号范围(十六进制) | UTF-8编码方式(二进制) |
---|---|
00000000-0000007F | 0xxxxxxx |
00000080-000007FF | 110xxxxx 10xxxxxx |
00000800-0000FFFF | 1110xxxx 10xxxxxx 10xxxxxx |
00010000-0010FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
以汉字“郑”为例:
它的unicode编码为90d1(1001000011010001)。根据上表,可以发现90d1处在第三行的范围,即格式是“1110xxxx 10xxxxxx 10xxxxxx”。然后,从“郑”的最后一个二进制位开始,依次从后向前填入格式中的x,多出的位补0。这样就得到了,“郑”的UTF-8编码为:
11101001 10000011 10010001
我们可以用下面一段代码证实:
public static void main(String[] args) throws UnsupportedEncodingException {
String zheng = "郑";
byte [] bs = new byte[3];
bs = zheng.getBytes("UTF-8");
for (int i = 0; i < bs.length; i++) {
System.out.print(convert(bs[i])+" ");
}
}
/**
* 返回整数byte的二进制补码
* @param number
* @return
*/
public static String convert(byte number){
int length = 8;//1个字节,8位长度
StringBuilder builder = new StringBuilder("");
for (int i = length-1; i >= 0; i--) {
//每一位和1比较,如果为0表示对应的位数为0,如果为1表示对应的位数为1
if(((1<<i)&number) == 0){
builder = builder.append("0");
}else {
builder = builder.append("1");
}
}
return builder.toString();
}
运行代码,输出:11101001 10000011 10010001
UTF-16是Unicode字符集的另一种转换方式,即把Unicode的码位转换为16比特长的码元串行,以用于数据存储或传递。
UTF-16的基本单位是2个字节,即16位。
2个字节能表达的最大十六进制数位0xFFFF。那么对于大于这个数的unicode编码,如何用utf-16表示?
因为unicode字符集的编码值范围为0-0x10FFFF,因而大于等于0x10000的辅助平面区的编码值无法用2个字节表示。
所以unicode标准规定:
基于多平面语言内,U+D800 - U+DFFF的值不对应于任何字符,为代理区。
因此,UTF-16利用保留下来的代理区的码位来对辅助平面的字符的码位进行编码。
第一个unicode平面(BMP),码位从U+0000至U+FFFF(除去代理区),包含了常用字符。UTF-16与UCS-2编码在这个范围内为单个16比特长的码元,数值等价于对应的码位。
即:在第一平面,unicode编码值是多少,相应的UTF-16的编码值也为多少。
辅助平面中的码元,大于等于0x10000,在UTF-16中被编码为一对16比特长的码元。很显然,每一个码元对应的码位值都应该在unicode第一平面的代理区内,从而保证UTF-16的正确映射。具体方法是:
码位减去0x10000,得到的值的范围为0-0xFFFFF(unicode最大码位为0x10FFFF,第一平面能表示的最大码位为0xFFFF,所以第二平面最小码位为0x10000)。写成二进制为yyyy yyyy yyxx xxxx xxxx
将高位的10比特值(yyyyyyyyyy)加上0xD800(1101100000000000)得到第一个码元,称作高位代理。值的范围为0xD800-0xDBFF。高位代理也称作前导代理。
将低位的10比特值(xxxxxxxxxx)加上0xDC00(1101110000000000),得到第二个码元,称作低位代理。值的范围为0xDC00-0xDFFFF。低位代理也称作后尾代理。
最终的UTF-16(4字节)的编码就是:110110yyyyyyyyyy 110111xxxxxxxxxx
可以看出,高位代理、低位代理、BMP中的有效字符的码位,三者是互不重叠的。
对于任意一个码元(2字节,辅助平面的值含有2个码元),仅可通过码元本省便可确定该码元所代表的含义。
这意味着UTF-16是自同步的:可以通过仅检查一个码元就可以判定给定字符的下一个字符的起始码元。
对于unicode第一平面的字符来说,UTF-8用1至3个字节来存储;UTF-16永远是用2个字节来存储,二进制值就是对应的unicode编码值。
同样以汉字“郑”为例:
它的unicode编码为0x90d1(1001000011010001)。所以它的UTF-16的编码同样为1001000011010001。
我们可以用上面的代码修改编码后证实:
public static void main(String[] args) throws UnsupportedEncodingException {
String zheng = "郑";
byte [] bs = new byte[3];
bs = zheng.getBytes("UTF-16");
for (int i = 0; i < bs.length; i++) {
System.out.print(convert(bs[i])+" ");
}
}
/**
* 返回整数byte的二进制补码
* @param number
* @return
*/
public static String convert(byte number){
int length = 8;//1个字节,8位长度
StringBuilder builder = new StringBuilder("");
for (int i = length-1; i >= 0; i--) {
//每一位和1比较,如果为0表示对应的位数为0,如果为1表示对应的位数为1
if(((1<<i)&number) == 0){
builder = builder.append("0");
}else {
builder = builder.append("1");
}
}
return builder.toString();
}
运行代码,输出:11111110 11111111 10010000 11010001
(11111110 11111111是UTF-16的文件标识,UTF-16文件的开头都会加上这个)
由于现实生活中常用的字符都是在unicode的第一平面内,由此可以看出。对于含有英文较多的文件,用UTF-8比较节省空间,对于含有中文较多的文件,用UTF-16比较节省空间。
对于unicode辅助平面的字符,其编码值大于等于0x10000,此时UTF-8同UTF-16一样都是用4个字节去存储。
这里以“?”为例:
它的unicode编码为0x22106(100010000000010110)
依然是上面的代码(仅以UTF-16为例):
public static void main(String[] args) throws UnsupportedEncodingException {
String zheng = "?";
byte [] bs = new byte[3];
bs = zheng.getBytes("UTF-16");
for (int i = 0; i < bs.length; i++) {
System.out.print(convert(bs[i])+" ");
}
}
/**
* 返回整数byte的二进制补码
* @param number
* @return
*/
public static String convert(byte number){
int length = 8;//1个字节,8位长度
StringBuilder builder = new StringBuilder("");
for (int i = length-1; i >= 0; i--) {
//每一位和1比较,如果为0表示对应的位数为0,如果为1表示对应的位数为1
if(((1<<i)&number) == 0){
builder = builder.append("0");
}else {
builder = builder.append("1");
}
}
return builder.toString();
}
运行代码,输出:11111110 11111111 11011000 01001000 11011100 00010110
去掉前面的UTF-16标识,得到:
11011000 01001000 11011100 00010110
第一个码元减去0xD800得到:00 01001000
第二个码元减去0xDC00得到:00 00010110
两者合并再加上0x10000得到“?”的unicode编码为:
10 00100000 00010110
即0x22016
java的每个字符都是对应一个unicode编码。准确的来讲,是第一平面的unicode编码。以下面的代码为例:
char c1 = '郑';//
char c2 = '?';//编译报错
java中,一个字符的存储长度为2个字节。由于“?”超过了2个字节能存储的范围,所以第二行即c2在编译期就报错。
再看一段代码:
System.out.println("郑".length());
System.out.println("?".length());
运行代码,输出:1、2
显然,字符串"郑"在java中占用了2个字节,"?"占用了4个字节。
注意:这里说的java字符编码不是指java文件的存储编码(文件存储编码是开发人员指定的,通常用UTF-8),而是指程序编译和执行的编码格式。
在java中,char类型的变量可以直接转换为整型变量,可以利用这一点了解到字符在java中的编码格式。
以汉字"郑"为例,将上面的代码加以修改:
public static void main(String[] args) throws UnsupportedEncodingException {
char c = '郑';
short s = (short) c;
System.out.print(convert(s));
}
/**
* 返回整数short的二进制补码
* @param number
* @return
*/
public static String convert(short number){
int length = 16;//2个字节,16位长度
StringBuilder builder = new StringBuilder("");
for (int i = length-1; i >= 0; i--) {
//每一位和1比较,如果为0表示对应的位数为0,如果为1表示对应的位数为1
if(((1<<i)&number) == 0){
builder = builder.append("0");
}else {
builder = builder.append("1");
}
}
return builder.toString();
}
运行代码,输出:1001000011010001
输出的二进制码刚好为“郑”对应的unicode编码。
再以辅助平面内的字符“?”为例:
由于char类型无法存储辅助平面内的字符,所以这里我们重新修改一下代码,利用字符串获取它在java中的存储编码。
在这个例子中,将字符“郑”也加进去,以比较在java中第一平面和辅助平面的存储方式之不同。
public static void main(String[] args) {
System.out.println(convert("郑"));
System.out.println(convert("?"));
}
/**
* 字符串转Unicode码
* @param str
* @return
*/
public static String convert(String str){
str = (str == null ? "" : str);
String tmp;
StringBuffer sb = new StringBuffer(1000);
char c;
int i, j;
sb.setLength(0);
for (i = 0; i < str.length(); i++){
c = str.charAt(i);
sb.append("\\u");
j = (c >>>8); //取出高8位
tmp = Integer.toHexString(j);
if (tmp.length() == 1)
sb.append("0");
sb.append(tmp);
j = (c & 0xFF); //取出低8位
tmp = Integer.toHexString(j);
if (tmp.length() == 1)
sb.append("0");
sb.append(tmp);
}
return (new String(sb));
}
运行代码,输出:
\u90d1
\ud848\udc16
将其转换成二进制分别为:
1001000011010001
1101100001001000 1101110000010110
与UTF-16一致!!!!
Unicdoe【真正的完整码表】对照表
Unicode编码表
JAVA 字符串编码总结
ASCII对照表
字符编码中ASCII、Unicode和UTF-8的区别
网页编码就是那点事
unicode、utf-8、ansi的故事及其相互转换
utf16编码格式