备注:
ASCII码:256个上限,当时已使用125个。由于加入其他字符导致256个远远不够,在1991年发布Unicode 1.0.
Unicode 1.0 的字符上限为65536个,在刚开始发布时只使用了不到一半。但当汉语,日语,汉语等表意字符加入时又不够用了。Unicode包含了ASCII码的全部内容。
Unicode也不是没有缺点的,英文和中文都占两个字节,小体积的程序还好,那如果10多个T的文件来说,使用Unicode进行编码都有可能造成了6个T的浪费。
所以为了解决问题,使用UTF-8编码。
UTF-8的优势在于,采用1~6个字节表示一个符号。根据不同符号改变不同字节长度。
但也不是非常完美。一个英文使用1个字节,但一个汉字需使用3个字节。但总体来说还是向好的。从Java SE 5.0开始,码点(与一个编码表中字符对应的标签)采用16进制,并且加入前缀U+。
比如拉丁字母A使用U+0041码点。
UTF-8 是目前最流行的编码(中国)。
一、char 码点和代码单元
在Java Core 卷1中 对Char的描述如下
在设计Java时决定采用16位的Unicode字符集....(中间省略)... 现在16位的Char类型已经不能满足描述所有Unicode字符的需要了。
Java为了解决这个问题的方法是使用码点和代码单元
代码点(Code Point):在 Unicode 代码空间中的一个值,取值 0x0 至 0x10FFFF,代表一个字符。
代码单元(Code Unit):在具体编码形式中的最小单位。比如 UTF-16 中一个 code unit 为 16 bits,UTF-8 中一个 code unit 为 8 bits。一个 code point 可能由一个或多个 code unit(s) 表示。在 U+10000 之前的 code point 可以由一个 UTF-16 code unit 表示,U+10000 及之后的 code point 要由两个 UTF-16 code units 表示
在Java中,char类型描述了UTF-16编码中的一个代码单元
码点:就是某个任意字符在Unicode编码表中对应的代码值
代码单元:是在计算机中用来表示码点的,大部分码点只需要一个代码单元表示,但是有一些是需要两个代码单元表示的。
不同码点对应的代码单元数量可能不同
下面实际操作体验一番
" " 这个数学符号码点 为U+1D546 在 Java中的代码单元为 U+D835 和U+DD46
String word = "\uD835\uDD46";
System.out.println("字符为:"+word + " String.length(): "+ word.length());
打印结果如下(String.length()返回的是字符串代码单元的长度)。
字符为: String.length(): 2
而且char类型无法放下这个符号
//char c = ' ';
String word2 = " ";
String firstUnit = Integer.toHexString(word.charAt(0));
String secondUnit = Integer.toHexString(word.charAt(1));
String codePoint = Integer.toHexString(word2.codePointAt(0));
System.out.println("第一个单元:" + firstUnit + " 第二个:" + secondUnit + " 码点:" +codePoint);
打印结果
第一个单元:d835 第二个:dd46 码点:1d546
二、UTF-16编码方式
我们来思考UTF-16的设计思路:
我们知道Unicode的范围为0x0~0x10FFFF
首先是0x0~0xFFFF这段区间,正好16位就可以表示,也兼容,两全其美
那么超过这个区间的怎么办呢?
也就是0xFFFF~0x10FFFF这段,我们先看这段区间有多少个码位,
0x10FFFF-0xFFFF=0x100000,那么这个十六进制表示的十进制也就是:1048576个码位
我们既然16位存不下,那肯定就是32位存咯,这个32能理解为什么不?不理解?是因为计算机只能以2的倍数拓展,如果不这么设计,就没办法解析。长短不一,不符合设计思路
32位来存这些数字,那么我们需要怎么存下呢,简单的思考过后,大家认为应该分开存储,也就是将32位分开前16位和后16位,每个16位各存一半
那么每一半存的就是1024(由来:1024^1024=1048576),1024代表的是2的10次幂,也就是10位二进制数
这样就知道了,32位二进制数字中,前后16位中各存10位就够用了,但是剩余的6位用来干什么呢?
和UTF-8的设计一样,为了让识别字符串变得容易(从文本的任意位置开始,均能区分一个字符的起始),这里是不是有点儿蒙?
举个栗子:
假设:
0000 0001 代表A
0000 0010 代表B
0000 0001 ,0000 0001 代表 X
0000 0010 ,0000 0001 代表Z
那么 ABXZ就是
0000 0001 ,0000 0010 , 0000 0001 ,0000 0001 , 0000 0010,0000 0001
A B X Z
但是让你从中间开始读取,当你读到X的时候,你不知道他是X还是 AB,这样就很麻烦,你需要设置标志,来让16位的数据的前8或后8不会和单个8位的重复
可以这样设计:
0xxx xxxx 代表0~2^7
11xx xxxx ,10xx xxxx 代表其他的
这样就能区分开了,当你读到11开头的,就代表他是16位的前8,10开头代表16位的后8
Ok,有了这个思路,我们就知道怎么设计刚才的那个6位了,当然是通过这6位来区分这16位数字代表的位置
也就是UTF-16中,表示数据有单16位和双16位(32位)两种,那么我们设计成单16位和32位中的前16位和后16位这三个16位完全不会重复,那么我们就能随时读到一组16位,就能知道他是单16还是前16还是后16
举个栗子:
根据上方信息,要求我们通过前6位来区分数据,那么前6位就是2^6=64,也就是开头数字的区间
我们设定如下:
54开头的为32位的前16位
55开头的为32位的后16位
其他开头的为单16位
这样我们就能区分开这三个16位了,在读取文档中的任意位置,都能随意区分出间隔咯
那么54开头的数据区间是多少呢,就是1101 10xx xxxx xxxx,区间就是D800~DBFF
那么55开头的数据区间是多少呢,就是1101 11xx xxxx xxxx,区间就是DC00~DFFF
为了配合UTF-16,Unicode中也将这两个区间屏蔽掉,不允许分配任何字符
下方为比较官方的关于UTF-16的编码详解
参考文献:
https://en.wikibooks.org/wiki/Unicode/Character_reference/D000-DFFF
具体编码方式
Unicode范围U+10000
~U+10FFFF
:将Unicode值减去(0x10000),得到20bit长的值。再将Unicode分为高10位和低10位。UTF-16编码的高位是2 Byte,高10位Unicode范围为0
-0x3FF
,将Unicode值加上0XD800
,得到高位代理(或称为前导代理,存储高位);低位也是2 Byte,低十位Unicode范围一样为0
~0x3FF
,将Unicode值加上0xDC00
,得到低位代理(或称为后尾代理,存储低位)
根据上面的转换方式,我们就能够将Unicode码根据UTF-16的编码方式进行转换。下面我们仍然通过两个例子来看下:
-
U+0020
,这个值的范围在第一部分,即经过UTF-16编码后,结果仍然为U+0020
,在内存中的顺序为00 20
。 -
U+12345
, 这个值的范围在第二部分,因此需要先减去0x10000
,得到0x02345
,拆分成高10位00 0000 1000
和低10位11 0100 0101
。根据上面规则加上特定值后,高位代理值为D808
,低位代理值为DF45
,最终内存中的顺序为D8 08 DF 45
。