字符集与字符编码(Unicode、UTF-8、UTF-16、UTF-32的编码逻辑)
字符集与字符编码
字符集:指的是⼀堆字符及其对应编号的集合,如:Unicode、ASCII、GBXXX
字符编码:指的是如何将某个字符的对应编号转换为计算机存储的⽅式,如:UTF-8等等
ASCII及其扩展
ASCII 码总共有128个,使⽤单字节表⽰,但由于不够⽤,因此之后陆续对ASCII产⽣了扩展,其中ISO-8859-1涵盖了⼤多数西欧语⾔字符,所以应⽤的最⼴泛,但仍然是单字节编码,总共能表⽰256个字符。
GBXXX
GBXXX是中国制订的汉字字符集和编码,以 GB2312 为例,既代表字符集也代表字符编码,该字符集收录的字符较少,所以使⽤ 1~2 个字节编码。
对于 ASCII 字符,使⽤⼀个字节存储,并且该字节的最⾼位是 0,即0-127和原来ASCII的意义相同,叫"半⾓"字符。
对于中国的字符,使⽤两个字节存储,并且规定每个字节的最⾼位都是 1,即两个⼤于127的字符连在⼀起时就表⽰⼀个汉字,叫"全⾓"字符。
Unicode
Unicode编码为表达任意语⾔的任意字符⽽设计,使⽤4字节(32位)的数字来表达全世界各个国家语⾔⾥的每个字母、符号(UNICODE 编码不超过0x10FFFF,即不超过21位)。
可以理解为Unicode是字符集,⽽UTF-32/ UTF-16/ UTF-8是其三种字符编码⽅案
UTF-32
因为Unicode使⽤4字节来表⽰⼀个字符,所以UTF-32就是直接使⽤4个字节来存储Unicode字符的⼀种编码⽅案。
优点:由于每4个字节为⼀个字符,因此获取字符串⾥的第N个字符的时间复杂度是O(1)。
缺点:占⽤较多空间。
可能会好奇:明明unicode不超过21位,那么使⽤3个字节(24位)就可以了,为何还要⽤4个字节的UTF-32呢?关于这个问题,需要看下⾯的UTF-16
UTF-16
由于⼤部分情况下不会⽤到65535以后的字符。因此,就有了另外⼀种Unicode编码⽅式,叫做UTF-16(16位,2字节)。
UTF-16将0–65535范围内的字符编码成2个字节,如果真的需要表达那些很少使⽤的超过这65535范围的Unicode字符,则需要使⽤技巧编码成4个字符,所以UTF-16也是变长编码,具体⽅案为:
如果字符编码U⼩于0x10000(0000 0000 ~ 0000 FFFF),也就是⼗进制的0到65535之内,则直接使⽤两字节表⽰;
如果字符编码U⼤于0x10000(0001 0000 ~ 0010 FFFF),共有0xFFFFF个编码,需要20个bit就可以表⽰这些编码,将其前
unicode汉字10 bit作为⾼位和16 bit的数值0xD800进⾏ 逻辑or 操作,将后10 bit作为低位和0xDC00做 逻辑or 操作,这样使⽤了4个字节来表
⽰这个字符的编码。
Unicode 编号范围(⼗六进制)具体的 Unicode 编号(⼆进制)UTF-16 编码编码后的字节数0000 0000 ~ 0000 FFFF xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx2
0001 0000 ~ 0010 FFFF
x xxxx xxxx xxxx xxxx xxxx
共有0xFFFFF个码,需要20个bit
110110yy yyyyyyyy
110111xx xxxxxxxx
(前10bit为y,后10bit为x)
4
Unicode 编号范围(⼗六进制)具体的 Unicode 编号(⼆进制)UTF-16 编码编码后的字节数
之所以可以使⽤110110yy yyyyyyyy 110111xx xxxxxxxx来表⽰超出65535范围的字符,是因为所有以110110或110111开头的Unicode 编码(0xD800~0xDFFF)是特别为四字节的UTF-16编码预留的,这个区间内没有收录任何字符。
优点:在空间效率上⽐UTF-32⾼,因为常⽤字符只需要2个字节来存储
缺点:UTF-16 存在⼤⼩端字节序问题(会在之后的BOM章节进⾏阐述)
UTF-8
UTF-8 使⽤1⾄4个字节为每个字符编码,是⼀种针对Unicode的可变长度字符编码,它可以⽤来表⽰Unicode标准中的任何字符,且其编码中的第⼀个字节仍与ASCII兼容,这使得原来处理ASCII字符的软件⽆须或只须做少部份修改,即可继续使⽤。
128个US-ASCII字符只需⼀个字节编码(Unicode范围由U+0000⾄U+007F)。
带有附加符号的拉丁⽂、希腊⽂、西⾥尔字母、亚美尼亚语、希伯来⽂、阿拉伯⽂、叙利亚⽂及它拿字母则需要⼆个字节编码(Unicode范围由U+0080⾄U+07FF)。
其他基本多⽂种平⾯(BMP)中的字符(这包含了⼤部分常⽤字)使⽤三个字节编码。
其他极少使⽤的Unicode辅助平⾯的字符使⽤四字节编码。
Unicode符号范围(⼗六进制)UTF-8编码⽅式(⼆进制)
00000000⾄0000007F0XXXXXXX
00000080⾄000007FF110XXXXX 10XXXXXX
00000800⾄0000FFFF1110XXXX 10XXXXXX 10XXXXXX
00010000⾄0010FFFF11110XXX 10XXXXXX 10XXXXXX 10XXXXXX
优点:UTF-8兼容ASCII,这是UTF-32和UTF-16都没有的;相⽐UTF-16也不再存在字节顺序的问题;具有良好的语种⽀持;使⽤英⽂和西⽂符号⽐较多的场景下(例如 HTML/XML),编码较短;由于是变长,字符空间⾜够⼤,未来 Unicode 新标准收录更多字符,UTF-8也能兼容
缺点:因为每个字符使⽤不同数量的字节编码,所以寻串中第N个字符的时间复杂度是O(n)。
BOM
对于UTF-32和UTF-16编码⽅式还有⼀些其他不明显的缺点,不同的计算机系统会以不同的顺序保存字节,这意味着字符如U+4E2D在UTF-16编码⽅式下可能被保存为4E 2D或者2D 4E,这取决于该系统使⽤的是⼤尾端(big-endian)还是⼩尾端(little-endian)。
为了解决这个问题,多字节的Unicode编码⽅式定义了⼀个"字节顺序标记(Byte Order Mark)",它是⼀个特殊的⾮打印字符,你可以把它包含在⽂档的开头来指⽰你所使⽤的字节顺序。
对于UTF-16,如果收到⼀个以字节FF FE开头的UTF-16编码的⽂档,由于FF⽐FE⼤,说明⼤的在前,⼩的在后,那么收到4E2D后就保存4E2D;反之,如果收到FE FF,说明⼩的在前,⼤的在后,那么收到4E2D就保存为2D4E。
Unicode编码BOM
UTF-8 without BOM⽆
UTF-8 with BOM EF BB BF
UTF-16LE FF FE
UTF-16BE FE FF
UTF-32LE FF FE 00 00
UTF-32BE00 00 FE FF
另:由于中⽂windows系统的默认字符集为GBK,在写⽂件给windows⽤户下载/读取的时候,可以加上BOM,这样⽤户在打开⽂件的时候,⼤部分程序会读取到BOM从⽽使⽤指定的编码⽅式打开,这样可以避免中⽂乱码的情况。
byte[] headerBuffer =new byte[]{(byte)0xEF,(byte)0xBB,(byte)0xBF};
out = OutputStream();
out.write(headerBuffer);
java中的字符
java中的String代表字符串,⼀个字符串其实就是字符数组,length()⽅法和charAt()⽅法都是针对字符。
java中使⽤char代表字符,java内码使⽤UTF-16(历史原因,Unicode⼀开始是设计成2字节的),⼀个char永远都是两个字节,对于超出Unicode 65535之后的字符,java需要使⽤两个char来存储(遵从UTF-16的逻辑,使⽤两个0xD800~0xDFFF的数来表⽰)。如果声明⼀个char变量为需要⽤两个char表⽰的字符,那么会报错
// error: Too many characters in character literal
// char c = ' ';
String a =" ";
System.out.println(a.length());
for(int i =0; i < a.length(); i++){
System.out.println((long)a.charAt(i));
}
System.out.Bytes(StandardCharsets.UTF_8)));
// 输出
// 2
// 55360
// 56321
// [-16, -96, -128, -127]
//  的Unicode码是U+20001
/
/ 这⾥的55360(D840)和56321(DC01)就是遵从UTF-16的规定将20001转换为两个0xD800~0xDFFF的数
// 由于getBytes()传⼊的是UTF-8,此时,会先根据55360和56321求出Unicode码值为U+20001,再根据UTF-8的规则,属于00010000⾄0010FFFF区间,因此会最终输出4个字节
java内部⼀直使⽤Unicode码,哪怕使⽤new String("你好".getBytes("gbk"), "gbk")在jvm中依然保存的是“你好”的Unicode码。
在使⽤getBytes("gbk")时,java会将传⼊字符串的Unicode码转成GBK码,该逻辑是在sun.DoubleByte类中实现的,具体怎么转的以后再研究。
byte只有8位,因此如果直接类型转换,对于0-255的字符不会有影响,但是对于(byte) '中'会丢失信息,原因是‘中’字的Unicode编码是(U+4E2D 0100 1110 0010 1101),已经超出了255,因此如果强制类型转换只会保留后8位即:0010 1101 = 45。java
的java.io.DataOutputStream中的writeBytes(String s)⽅法就有这个问题,会导致中⽂出现乱码:
public final void writeBytes(String s)throws IOException {
int len = s.length();
for(int i =0; i < len ; i++){
out.write((byte)s.charAt(i));
}
incCount(len);
}
因此在使⽤DataOutputStream时最好不要使⽤该⽅法。
HTTP中的字符集与编码
Accept-Charset:浏览器申明⾃⼰接收的字符集,这就是本⽂前⾯介绍的各种字符集和字符编码,如gb2312,utf-8(通常我们说Charset包括了相应的字符编码⽅案);
Accept-Encoding:浏览器申明⾃⼰接收的编码⽅法,通常指定压缩⽅法,是否⽀持压缩,⽀持什么压缩⽅法(gzip,deflate),(注意:这不是只字符编码);
Accept-Language:浏览器申明⾃⼰接收的语⾔。语⾔跟字符集的区别:中⽂是语⾔,中⽂有多种字
符集,⽐如
big5,gb2312,gbk等等;
Content-Type:WEB服务器告诉浏览器⾃⼰响应的对象的类型和字符集。例如:Content-Type: text/html; charset=‘gb2312’
Content-Encoding:WEB服务器表明⾃⼰使⽤了什么压缩⽅法(gzip,deflate)压缩响应中的对象。例如:Content-Encoding:gzip
Content-Language:WEB服务器告诉浏览器⾃⼰响应的对象的语⾔。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。