ASCII、Unicode、UTF-8以及彼此的联系
ASCII、Unicode、UTF-8以及彼此的联系
1.ASCII
1.1.ASCII简介
如果你稍微了解计算机的原理,那么你应该了解,一切信息在计算机内部都是以0和1即二进制值表示。在计算中,最小的信息单位为位(bit),而最小的存储单元为字节,即时你只想存储0或1,但是也需要一个字节的空间。
上个世纪60年代,为了对应英文字符与二进制位之间的关系,美国制定了一套字符编码集,这就是赫赫有名的ASCII码(American Standard Code for Information Interchange,美国信息交换标准码),一直沿用至今。它只是用了一个字节,确切地说,只使用了一半的字节,我们都知道1个字节有8位,既可以表示256个对应关系,但是对于以英语为母语的美国来说,用来映射英文字母和一切特殊字符,简直是绰绰有余,因此,他们规定闲置第一个比特(位),,只使用剩余的7个比特。比如大写的字母 A—Z 对应十进制的65—90、小写字母 a—z 对应十进制的97—122,数字字符 0—9 对应十进制的48—57,以及32个不能打印出来的控制符也有相应的对应,等等。
1.2.ASCII的局限性
英语用128个符号编码就够了,但是用来表示其他语言,128个符号是不够的。比如,在法语中,字母上方有注音符号,它就无法用 ASCII 码表示。于是,一些欧洲国家就决定,利用字节中闲置的最高位编入新的符号。比如,法语中的é的编码为130(二进制10000010)。这样一来,这些欧洲国家使用的编码体系,可以表示最多256个符号。
但是,这里又出现了新的问题。不同的国家有不同的字母,因此,哪怕它们都使用256个符号的编码方式,代表的字母却不一样。比如,130在法语编码中代表了é,在希伯来语编码中却代表了字母Gimel (ג),在俄语编码中又会代表另一个符号。但是不管怎样,所有这些编码方式中,0–127表示的符号是一样的,不一样的只是128–255的这一段。
至于亚洲国家的文字,使用的符号就更多了,汉字就多达10万左右。一个字节只能表示256种符号,肯定是不够的,就必须使用多个字节表达一个符号。比如,简体中文常见的编码方式是 GB2312,使用两个字节表示一个汉字,所以理论上最多可以表示 256 x 256 = 65536 个符号,但也不足以存储所有字符。
为了解决兼容和统一,Unicode编码应运而生。
2.Unicode
2.1.Unicode简介
Unicode(Universal Multiple-Octet Coded Character Set,通用多八位编码字符集) ,简称为UCS。UCS可以看作是 “Unicode Character Set” 的缩写。它是全球文字统一编码,它由国际组织设计,可以容纳全世界所有语言文字的编码方案,它把世界上的各种文字的每一个字符指定唯一编码。
2.2.Unicode解决了什么问题
Unicode 是全球文字统一编码,它由国际组织设计,可以容纳全世界所有语言文字的编码方案,它把世界上的各种文字的每一个字符指定唯一编码,在现在的计算机内存中,统一使用Unicode编码,因此可以实现跨语种、跨平台的应用。实现了兼容和统一。
2.3.UCS-2和UCS-4
UCS-2 (Universal Character Set coded in 2 octets),顾名思义,UCS-2是用两个字节来表示代码点,其取值范围为 U+0000~U+FFFF。但是两个字节最多只能表示65535个对应关系。
为了能表示更多的字符,人们又提出了UCS-4,即用四个字节表示代码点。它的范围为 U+00000000~U+7FFFFFFF,其中 U+00000000~U+0000FFFF和UCS-2的对应关系是相同的,至于UCS-4的上限为什么不是 U+FFFFFFFF 而是 U+7FFFFFFF,这是因为UCS-4规定最高位的第一个字节必须为0
在现在的Windows系统上采用的是UCS-4字符集,这就是为什么我们可以在Windows的图形界面能显示所有字符的原因。
2.4.Little-endian和Big-endian
既然字符的对应Unicode码有可能会是两个或更多个字节,那么,当涉及到存储和传输时,我们就应该考虑到,高字节在前还是低字节在前,不管是哪个,只有在传输和接受两段采取相同的策略时,才能正确地解析。
Little endian 和 Big endian这两个古怪的名称来自英国作家斯威夫特的小说《格列佛游记》,书中描述了小人国里对于吃鸡蛋时究竟是从大头(Big-endian)敲开还是从小头(Little-endian)敲开而多次引起了争论和内战。
- Big endian:大头,即高字节在前。
- Little endian:小头,即低字节在前。
2.5.Unicode中文的注意事项
在Java中我们经常使用 char字符值 的范围是否处于 [\u4e00,\u9fa5] 区间内来判断它是否是中文,这其实并不严谨,这只能判断基本汉字,因为[\u4e00,\u9fa5] 只囊括了 20902 个基本汉子,中文字符的数量远远不止如此,有些汉子已经超过了两个字节。但不得否认的是,对于我们大多数人来说, [\u4e00,\u9fa5] 已经足够了,普通人基本不会使用到之外的其他中文字符。
下面给出较全面的汉字 Unicode 编码范围供了解:
如有兴趣可以进入汉字 Unicode 编码范围网站点击查看,另附汉字字符集编码查询网站供查询使用。
还有一点值得注意的是,在 Java 编程中,使用 char 类型表示字符,而 String 底层就是 char 数组,因此对于单个字符的处理要求是不能超过2个字节,我们知道从上面我们可以看到,有写中文字符已经超过两个字节,比如 “????”,它对应 \u2A700,但是当我们尝试将它复制到 Java 代码的双引号("")之内时,神奇的事情发生了,显示的并不是"????",而是 “\uD869\uDF00”,因为是它超出了两个字节,因此它当成了2个字符,但是可以正常输出,因为计算机底层能够识别,至于UCS-2 与 UCS-4 之间是如何转换的,这里不做探讨。
String s = "\uD869\uDF00";
System.out.println(s); //????
System.out.println(s.length()); //2
2.6.Unicode没有解决的问题
Unicode 只是一个字符集,它只规定了每个字符和二进制数的对应关系,却没有规定这个二进制数应该如何存储和传输。想象一下,有的字符如ASCII码中对应的字符,只需要一些字节,但有些中文需要两个或者三个字节,如何进行区别呢,计算机如何知道三个字节表示一个符号,而不是分别表示三个符号呢?而且,我们已经知道,英文字母只用一个字节表示就够了,如果 Unicode 统一规定,每个符号用三个或四个字节表示,那么每个英文字母前都必然有二到三个字节是0,这对于存储来说是极大的浪费,大小会因此大出二三倍,这是无法接受的。
因此就出现了另外一个概念UTF(UCS Transformation Format,Unicode转换格式),它规定了对应二进制的保存的传输的格式。常见的UTF规范包括UTF-8、UTF-7、UTF-16。
3.UTF-8
3.1.UTF-8简介
UTF-8 就是以 8位(一个字节) 为单元对 UCS 进行格式转换。注意:UTF-8 是一种Unicode转换格式,一种数据的存储和传输方式,只发生在边界的地方,也就是各种输入/输出流的起作用的地方。
UTF-8是一种变长字节编码方式。对于某一个字符的UTF-8编码,如果只有一个字节则其最高二进制位为0;如果是多字节,其第一个字节从最高位开始,连续的二进制位值为1的个数决定了其编码的位数,其余各字节均以10开头。UTF-8最多可用到6个字节。
如下所示:
1字节 0xxxxxxx
2字节 110xxxxx 10xxxxxx
3字节 1110xxxx 10xxxxxx 10xxxxxx
4字节 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
5字节 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
6字节 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
3.2.UTF-8和Unicode之间的转换
在 Java 的 JDK 中,如果你看过 DataOutputStream 对 DataOutput 的 writeUTF(String s) 方法以及 DataInputStream 对 DataInput 的 readUTF()方法的源码实现,那么你可能已经知道了 UTF-8 和 Unicode 的转换机制是如何工作的,如果你对此并不熟悉并且有兴趣,你不妨看一看下列的描述。
在这之前,先熟悉一下下列值:
我们通过 Java 保存字符串到文件,以及从文件中读取字符串的例子来讲解 UTF-8 和 Unicode 的转换:
显示将保存字符串,即 UTF-8 转为 Unicode,根据 JDK 的实现,大致思路如下:
1.先计算存储此字符串需要的字节数utflen,然后将utflen放在写在字符串转换后的数据的前面,方便解析的时候使用:
//str为要存储的字符串
int strlen = str.length();
int utflen = 0;
int c = 0;
//遍历字符串中的每个字符,判断各个字符需要几个字符存储
for (int i = 0; i < strlen; i++) {
c = str.charAt(i);
if ((c >= 0x0001) && (c <= 0x007F)) { //如果字符值处在[0, 127], 则需要一个字节存储
utflen++;
} else if (c > 0x07FF) { //如果字符值大于0x07FF即2047,则需要3个字节存储
utflen += 3;
} else { // 否则需要2个字节存储
utflen += 2;
}
}
2.将每个字符根据值大小选择对应的UTF-8格式长度模板
for (;i < strlen; i++){
c = str.charAt(i);
if ((c >= 0x0001) && (c <= 0x007F)) {//[0, 127],一个字节的模板即可存储
bytearr[count++] = (byte) c;
} else if (c > 0x07FF) {//大于2047,需要3个字节的模板才可存储
bytearr[count++] = (byte) (0xE0 | ((c >> 12) & 0x0F));
bytearr[count++] = (byte) (0x80 | ((c >> 6) & 0x3F));
bytearr[count++] = (byte) (0x80 | ((c >> 0) & 0x3F));
} else {//其他的则使用2个字节的模板存储
bytearr[count++] = (byte) (0xC0 | ((c >> 6) & 0x1F));
bytearr[count++] = (byte) (0x80 | ((c >> 0) & 0x3F));
}
}
例如 “汉” 字符的 Unicode 编码是6C49。6C49 在 0800-FFFF 之间,所以肯定要用 3 字节的模板了:1110xxxx 10xxxxxx 10xxxxxx。将6C49写成二进制是:0110 110001 001001, 依次代替模板中的x,得到:11100110 10110001 10001001,即E6 B1 89。
UTF-8格式转换可能会涉及到更多的字节,但是原理相同,为了方便演示,只探讨UCS-2,那么最多对应UTF-8 转换格式的 3 个字节。