Java中Unicode与utf-8、utf-16的关系
转载于:https://www.cnblogs.com/fnlingnzb-learner/p/6163205.html
基础知识:
字符集合(ASCII、Unicode):由一套用于特定用途的字符组成,例如支持西欧语言的字符集合,支持中文的字符集合。字符集合只定义了符号和他们的语意,其实跟计算机没有直接关系。现实生活中,不同的语系有自己的字符集合,例如藏文有自己的字符集合,汉文有自己的字符集合。到计算机的世界中,也有各种字符集合,例如ASCII字符集合,GB2312字符集合,GBK字符集合。还有一个其他字符集合的超集--Unicode字符集定义了几乎绝大部分现存语言需要的字符,是一种通用的字符集,来支持多语言环境(可以同时处理多种语言混合的情况)。各个国家和地区在制定编码标准的时候,“字符集合”和“字符编码”一般都是同时制定的。所以像ASCII字符集合一样,它也同时代表了一种字符的编码。
字符编码:是一套规则,定义了在计算机内存中如何表示字符,是字符集中的每个字符与计算机内存中字节之间的转换关系,也可以认为是把字符数字化,规定每个“字符”分别用一个字节还是多个字节存储,用哪些字节来存储。例如ASCII编码[你没看错,它既是一种字符集合,也是一种字符编码],定义了英文字母和符号在计算机中的表示方式,是用一个字节来表示。Unicode字符集合,有好几种字符编码方式,例如变长度编码的UTF8,UTF16等。中文字符集也有很多字符编码,例如上文提到的GB2312编码,GBK编码等。
char、unicode、string和UTF8、UTF16之间的关系描述:Java语言内部使用的就是16位的Unicode编码,从概念上讲java字符串就是Unicode字符序列,Unicode字符集合的码点(码点:指与一个编码表中的某个字符对应的代码值)可以分成17个代码级别,第一个代码级别称为基本的多语言级别,码点从U+0000到U+FFFF,即65536个码点,只有第一级别的码点可以用一个char值表示;其余的16个级别码点从U+10000到U+10FFFF,其中包含一些辅助字符,需要两个char值才能表示一个码点;Unicode只是一个符号集,它只规定了符号的二进制代码,却没有规定这个二进制代码应该如何存储,二进制与16进制等可以灵活转换,只是数值的表示方式变化而已,其值的大小不变。
UTF8、UTF16即为采用什么样的规则表示所有的码点,在UTF16的编码规则中,UTF16采用不同长度的编码表示所有的Unicode码点,在基本的多语言级别,每个字符用16位表示,通常被称为代码单元;而辅助字符采用一对连续的代码单元进行编码。U+D800~U+DFFF为空闲的2048个替代区域,U+D800~U+DBFF用于第一个代码单元,U+DC00~U+DFFF用于第二个代码单元,这样的设计可以迅速的知道一个代码单元是一个字符的编码还是一个辅助字符的第一或第二部分。
为啥需要Unicode
我们知道计算机其实挺笨的,它只认识0101这样的字符串,当然了我们看这样的01串时肯定会比较头晕的,所以很多时候为了描述简单都用十进制,十六进制,八进制表示.实际上都是等价的,没啥太多不一样.其他啥文字图片之类的其他东东计算机不认识.那为了在计算机上表示这些信息就必须转换成一些数字.你肯定不能想怎么转换就怎么转,必须得有定些规则.于是刚开始的时候就有ASCII字符集(American Standard Code for Information Interchange, "美国信息交换标准码),它使用7 bits来表示一个字符,总共表示128个字符,我们一般都是用字节(byte,即8个01串)来作为基本单位.那么怎么当用一个字节来表示字符时第一个bit总是0,剩下的七个字节就来表示实际内容.后来IBM公司在此基础上进行了扩展,用8bit来表示一个字符,总共可以表示256个字符.也就是当第一个bit是0时仍表示之前那些常用的字符.当为1时就表示其他补充的字符.
英文字母再加一些其他标点字符之类的也不会超过256个.一个字节表示主足够了.但其他一些文字不止这么多 ,像汉字就上万个.于是又出现了其他各种字符集.这样不同的字符集交换数据时就有问题了.可能你用某个数字表示字符A,但另外的字符集又是用另外一个数字表示A.这样交互起来就麻烦了.于是就出现了Unicode和ISO这样的组织来统一制定一个标准,任何一个字符只对应一个确定的数字.ISO取的名字叫UCS(Universal Character Set),Unicode取的名字就叫unicode了.
总结起来为啥需要Unicode就是为了适应全球化的发展,便于不同语言之间的兼容交互,而ASCII不再能胜任此任务了.
Unicode详细介绍
1.容易产生后歧义的两字节
unicode的第一个版本是用两个字节(16bit)来表示所有字符
.实际上这么说容易让人产生歧义,我们总觉得两个字节就代表保存在计算机中时是两个字节.于是任何字符如果用unicode表示的话保存下来都占两个字节.其实这种说法是错误的.
其实Unicode涉及到两个步骤,首先是定义一个规范,给所有的字符指定一个唯一对应的数字,这完全是数学问题,可以跟计算机没半毛钱关系.第二步才是怎么把字符对应的数字保存在计算机中,这才涉及到实际在计算机中占多少字节空间.
所以我们也可以这样理解,Unicode是用0至65535之间的数字来表示所有字符.其中0至127这128个数字表示的字符仍然跟ASCII完全一样.65536是2的16次方.这是第一步.第二步就是怎么把0至65535这些数字转化成01串保存到计算机中.这肯定就有不同的保存方式了.于是出现了UTF(unicode transformation format),有UTF-8,UTF-16.
2.UTF-8 与UTF-16的区别
UTF-16比较好理解,就是任何字符对应的数字都用两个字节来保存.我们通常对Unicode的误解就是把Unicode与UTF-16等同了.但是很显然如果都是英文字母这做有点浪费.明明用一个字节能表示一个字符为啥整两个啊.
于是又有个UTF-8,这里的8非常容易误导人,8不是指一个字节,难道一个字节表示一个字符?实际上不是.当用UTF-8时表示一个字符是可变的,有可能是用一个字节表示一个字符,也可能是两个,三个..反正是根据字符对应的数字大小来确定.
于是UTF-8和UTF-16的优劣很容易就看出来了.如果全部英文或英文与其他文字混合,但英文占绝大部分,用UTF-8就比UTF-16节省了很多空间.而如果全部是中文这样类似的字符或者混合字符中中文占绝大多数.UTF-16就占优势了,可以节省很多空间.另外还有个容错问题,等会再讲
看的有点晕了吧,举个例子.假如中文字"汉"对应的unicode是6C49(这是用十六进制表示,用十进制表示是27721为啥不用十进制表示呢?很明显用十六进制表示要短点.其实都是等价的没啥不一样.就跟你说60分钟和1小时一样.).你可能会问当用程序打开一个文件时我们怎么知道那是用的UTF-8还是UTF-16啊.自然会有点啥标志,在文件的开头几个字节就是标志.
EF BB BF 表示UTF-8
FE FF 表示UTF-16.
用UTF-16表示"汉"
假如用UTF-16表示的话就是01101100 01001001(共16 bit,两个字节).程序解析的时候知道是UTF-16就把两个字节当成一个单元来解析.这个很简单.
用UTF-8表示"汉"
用UTF-8就有复杂点.因为此时程序是把一个字节一个字节的来读取,然后再根据字节中开头的bit标志来识别是该把1个还是两个或三个字节做为一个单元来处理.
0xxxxxxx,如果是这样的01串,也就是以0开头后面是啥就不用管了XX代表任意bit.就表示把一个字节做为一个单元.就跟ASCII完全一样.
110xxxxx 10xxxxxx.如果是这样的格式,则把两个字节当一个单元
1110xxxx 10xxxxxx 10xxxxxx 如果是这种格式则是三个字节当一个单元.
这是约定的规则.你用UTF-8来表示时必须遵守这样的规则.我们知道UTF-16不需要用啥字符来做标志,所以两字节也就是2的16次能表示65536个字符.
而UTF-8由于里面有额外的标志信息,所有一个字节只能表示2的7次方128个字符,两个字节只能表示2的11次方2048个字符.而三个字节能表示2的16次方,65536个字符.
由于"汉"的编码27721大于2048了所有两个字节还不够,只能用三个字节来表示.
所有要用1110xxxx 10xxxxxx 10xxxxxx这种格式.把27721对应的二进制从左到右填充XXX符号(实际上不一定从左到右,也可以从右到左,这是涉及到另外一个问题.等会说.
刚说到填充方式可以不一样,于是就出现了Big-Endian,Little-Endian的术语.Big-Endian就是从左到右,Little-Endian是从右到左.
由上面我们可以看出UTF-8在局部的字节错误(丢失、增加、改变)不会导致连锁性的错误,因为 UTF-8 的字符边界很容易检测出来,所以容错性较高。
Unicode版本2
前面说的都是unicode的第一个版本.但65536显然不算太多的数字,用它来表示常用的字符是没一点问题.足够了,但如果加上很多特殊的就也不够了.于是从1996年开始又来了第二个版本.用四个字节表示所有字符.这样就出现了UTF-8,UTF16,UTF-32.原理和之前肯定是完全一样的,UTF-32就是把所有的字符都用32bit也就是4个字节来表示.然后UTF-8,UTF-16就视情况而定了.UTF-8可以选择1至8个字节中的任一个来表示.而UTF-16只能是选两字节或四字节..由于unicode版本2的原理完全是一样的,就不多说了.
前面说了要知道具体是哪种编码方式,需要判断文本开头的标志,下面是所有编码对应的开头标志
EF BB BF UTF-8
FE FF UTF-16/UCS-2, little endian
FF FE UTF-16/UCS-2, big endian
FF FE 00 00 UTF-32/UCS-4, little endian.
00 00 FE FF UTF-32/UCS-4, big-endian.
其中的UCS就是前面说的ISO制定的标准,和Unicode是完全一样的,只不过名字不一样.ucs-2对应utf-16,ucs-4对应UTF-32.UTF-8是没有对应的UCS
UTF-16 并不是一个完美的选择,它存在几个方面的问题:
- UTF-16 能表示的字符数有 6 万多,看起来很多,但是实际上目前 Unicode 5.0 收录的字符已经达到 99024 个字符,早已超过 UTF-16 的存储范围;这直接导致 UTF-16 地位颇为尴尬——如果谁还在想着只要使用 UTF-16 就可以高枕无忧的话,恐怕要失望了
- UTF-16 存在大小端字节序问题,这个问题在进行信息交换时特别突出——如果字节序未协商好,将导致乱码;如果协商好,但是双方一个采用大端一个采用小端,则必然有一方要进行大小端转换,性能损失不可避免(大小端问题其实不像看起来那么简单,有时会涉及硬件、操作系统、上层软件多个层次,可能会进行多次转换)
- 另外,容错性低有时候也是一大问题——局部的字节错误,特别是丢失或增加可能导致所有后续字符全部错乱,错乱后要想恢复,可能很简单,也可能会非常困难。(这一点在日常生活里大家感觉似乎无关紧要,但是在很多特殊环境下却是巨大的缺陷)
反过来 UTF-8 也不完美,也存在一些问题:
- 文化上的不平衡——对于欧美地区一些以英语为母语的国家 UTF-8 简直是太棒了,因为它和 ASCII 一样,一个字符只占一个字节,没有任何额外的存储负担;但是对于中日韩等国家来说,UTF-8 实在是太冗余,一个字符竟然要占用 3 个字节,存储和传输的效率不但没有提升,反而下降了。所以欧美人民常常毫不犹豫的采用 UTF-8,而我们却老是要犹豫一会儿
- 变长字节表示带来的效率问题——大家对 UTF-8 疑虑重重的一个问题就是在于其因为是变长字节表示,因此无论是计算字符数,还是执行索引操作效率都不高。为了解决这个问题,常常会考虑把 UTF-8 先转换为 UTF-16 或者 UTF-32 后再操作,操作完毕后再转换回去。而这显然是一种性能负担。
当然,UTF-8 的优点也不能忘了:
- 字符空间足够大,未来 Unicode 新标准收录更多字符,UTF-8 也能妥妥的兼容,因此不会再出现 UTF-16 那样的尴尬
- 不存在大小端字节序问题,信息交换时非常便捷
- 容错性高,局部的字节错误(丢失、增加、改变)不会导致连锁性的错误,因为 UTF-8 的字符边界很容易检测出来,这是一个巨大的优点(正是为了实现这一点,咱们中日韩人民不得不忍受 3 字节 1 个字符的苦日子)
那么到底该如何选择呢?
因为无论是 UTF-8 和 UTF-16/32 都各有优缺点,因此选择的时候应当立足于实际的应用场景。例如在我的习惯中,存储在磁盘上或进行网络交换时都会采用 UTF-8,而在程序内部进行处理时则转换为 UTF-16/32。对于大多数简单的程序来说,这样做既可以保证信息交换时容易实现相互兼容,同时在内部处理时会比较简单,性能也还算不错。(基本上只要你的程序不是 I/O 密集型的都可以这么干,当然这只是我粗浅的认识范围内的经验,很可能会被无情的反驳)
稍微再展开那么一点点……
在一些特殊的领域,字符编码的选择会成为一个很关键的问题。特别是一些高性能网络处理程序里更是如此。这时采用一些特殊的设计技巧,可以缓解性能和字符集选择之间的矛盾。例如对于内容检测/过滤系统,需要面对任何可能的字符编码,这时如果还采用把各种不同的编码都转换为同一种编码后再处理的方案,那么性能下降将会很显著。而如果采用多字符编码支持的有限状态机方案,则既能够无需转换编码,同时又能够以极高的性能进行处理。当然如何从规则列表生成有限状态机,如何使得有限状态机支持多编码,以及这将带来哪些限制,已经又成了另外的问题了。