欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

ASCII、Unicode、UTF-8以及彼此的联系

程序员文章站 2022-07-15 13:07:02
...

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 编码范围供了解:
ASCII、Unicode、UTF-8以及彼此的联系
如有兴趣可以进入汉字 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 的转换机制是如何工作的,如果你对此并不熟悉并且有兴趣,你不妨看一看下列的描述。

在这之前,先熟悉一下下列值:
ASCII、Unicode、UTF-8以及彼此的联系

我们通过 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 个字节。