参考文献(参考文章较多,未一一列出):
文章目录
1. 字符集和字符编码
1.1 字节 字符
字节是计算机存储容量的单位. 1 字节 = 8 位(二进制位)
字符:任何一个文字或者一个符号都是一个字符
1.2 字符集 和 字符编码
字符集
- 在一个字库表中,每个字符都有一个对应的二进制地址(即二进制表示方式),而编码字符集就是这些地址的集合.
- 比如ASCII编码字符集中 A->(0100 0001)65.
- 字符集和字库表(比如汉字表)一一对应,相互转换.
字符编码
- 直接使用字符对应的二进制地址来显示文字是十分浪费的,Unicode 编码(包含了各种语言中使用到的所有’字符’)规范中包括了几百万个字符,想要全部包括这些字符,起码需要3字节(2^24=16777216)的容量,为了方便以后的扩展,还将保留更多未使用的空间,最多可以存储4个字节的容量.
- 因此,为了区分每个字符,哪怕是 00000000 00000000 00000000 00001111 这样的字符(实际只用了一个字节的字符),也要使用4字节的空间,导致了由这类字符组成1G的文件,需要4G才能存储,极其浪费.
- 于是,就出现了同一编码规范,不同的编码方式. 比如Unicode这个编码规范有 UTF-8,UTF-16,UTF-32这几种编码方式.不同的编码方式,节约空间不同.
字符与编码的发展历程
系统内码 | 说明 | 系统 |
---|---|---|
ASCII | 计算机刚开始只支持英文 | 英文DOS |
ANSI编码(本地化) 0x00~0x7f(127):1个字节表示英文字符(ASCII) 0x80~0xFFFF(扩展的ASCII码) | 为了计算机支持更多的语言,通常使用0x80~0xFFFF范围内的两个字节来表示1个字符。比如:“中”在中文系统中使用[0xD6,0xD0]表示. 不同的国家执行不同的标准,由此产生了GB2312,BIG5等各自的标准.使用2个字节表示一个字符的编码方式称为ANSI编码.中文简体系统下,ANSI表示GB2312. 不同的ANSI编码之间互不兼容,信息在国际间交流时,无法将两种语言文字存储在同一段的ANSI编码文本中. | 中文DOS,中文Windows等 |
UNICODE(国际化) | 为了使国际间的信息交流更加方便,国际组织制定了 UNICODE字符集,为各种语言的每个字符设定了统一并且唯一的编号,以满足跨平台的、跨语言的需求. | Linux,Java |
1.3 内码
参考: Java中char占用几个字节
内码 :某种语言运行时,其char和 String 在内存中的编码方式。
Java 中的 char 内码使用 utf-16 编码方式.
utf-16:占用 2 / 4 个字节.
当 utf-16 为 4字节时,使用一对 char (具体如何使用没看懂,意思是使用 String ?)
https://docs.oracle.com/javase/tutorial/i18n/text/unicode.html
外码 :除了内码,皆是外码
Java的class文件采用 UTF8 来存储字符,也就是说,class中字符占1~6个字节。
Java序列化时,字符也采用 UTF8 编码,占1~6个字符。
2. 编码方式
2.1 ASCII码
最早的编码规范, 0000 0000 ~ 0111 1111 (0~127,0x7f).可以表示阿拉伯数字和大小写英文字母,以及一些简单的符号标点等. 占用大小 1 字节.
(American Standard Code for Information Interchange美国信息交换标准代码)
2.2 ISO-8859-1
- 使用 单字节 内的所有空间(8位,0~255),ASCII编码 7 位.
- 包括ASCII码以外的 西欧、希腊语…等对应的文字符号,不支持中文.
- 在支持 ISO-8859-1 的系统中传输和存储其他任何编码的字节流都不会被抛弃,换言之,把其他任何编码的字节流当作 ISO-8859-1 编码看待都没有问题.
- 上面的这个特性应用: MySQL 默认编码 Latin1 就是利用了这个特性.
- 有时候 tomcat 也使用 ISO-8859-1 编码.
2.3 ANSI 编码规范
- 编码方式: GB2312 , BIG5, SHIFT_JIS , ISO-8859-2
1. GB2312
- 规定ASCII 码中127之后的奇异符号直接取消掉,127(0x7f)之前的字符和原来的含义相同,两个大于 127 的字符连在一起表示一个汉字.
- 前面的一个字节(高字节)从 0xA1~0xF7 ,后面一个字节(低字节) 从 0xA1~0xFE,这样就能组合出大约 7000多个简体汉字了.
- 这些编码里,把数学符号,罗马希腊字母都编写进去了,连ASCII里本来就有的数组,标点,字母都统统重新编了 2 个字节长的编码.这就是常说的 全角,原来127之前的叫做 半角.
- 这种汉字方案就叫 GB2312,是对ASCII的扩展,并兼容 ASCII.
2. GBK
- GBK 字符集所有字符占 2 个字节,不论中文还是英文.
- 许多人名打不出来,即GB2312 的汉字不够用了,将 GB2312 扩展后称为 GBK.增加了 近20000个新汉字.
- 支持国际标准 ISO/IEC40646-1 和 国家标准 GB13000-1 中的全部中日韩汉字.
3. GBK18030
- 少数名族文字无法在电脑显示,然后将 GBK 扩展成 GB18030.
- GB2312,GBK,GB18030 都是双字节编码,称为 DBCS(double byte character set,双字节字符集).
- 汉字和英文字符并存,如何区分:值>127,则认为一个双字节字符集里的字符出现了.但在 unicode 中不是这样的.
2.4 UNICODE 编码规范
- 编码方式: UTF-8 , UTF-16, UnicodeBig 等
0. Unicode 的体系结构
Unicode 也称为 UCS(Universal Coded Character Set:国际编码字符集合) 是一个字符集合,对世界上大部分的文字系统进行了整理,编码,使电脑可以用更为简单的方式来呈现和处理文字。最新的版本 Unicode 11.0 已经包含了 137439 个字符。Unicode 的数量之多,如果完全涵盖它, 需要用 4 个字节来表示,但是计算机存储过程中却不是必须都用 4 个字节来完成(2^32 = 4,294,967,296)。对于有些字符,尤其是编码在前面的字符我们也可以通过 1 个或 两个字节来节省空间。这就涉及到了 unicode 的实现方式。
Unicode 常用的编码方式有 UTF-8, UCS-2, UTF-16 三种,另外还有一种 UTF-32 虽然不太常用也需要提一下。
UTF-8: 能够完全兼容 ASCII. 使用最为广泛的编码方式.
UCS-2:使用 2 字节 来表示字符,也就是只能表示 65536 个 字符,它只能表示 BMP 中的字符.当前 unicode 字符数量远超过了 UCS-2,此时新的编码方式出现了 UTF-16.
UTF-16:为了解决 UCS-2 编码问题产生的,扩展自 UCS-2
- 基本多文种平面中,与 UCS-2 编码完全一致,使用两个字节表示
U+010000
到U+10FFFF
范围 使用 4 个字节表示
UTF-32:UTF-32 对 Unicode 中的每个字符都用 4 个字节来表示,占用的空间比其他编码要多的多,也正是这个原因,人们才用的很少。
1. Unicode 和 BigEndianUnicode
-
这两个指示了存储顺序不同
A 的 Unicode -> 6500
A 的 BigEndianUnicode -> 0065
endian 可以翻译成字节序.big endian(大尾)和little endian(小尾)是CPU处理多字节数的不同方式。例如“汉”字的Unicode编码是6C49。那么写到文件里时,究竟是将6C写在前面,还是将49写在前面?如果将6C写在前面,就是big endian。还是将49写在前面,就是little endian。
2.UTF
在Unicode里,所有的字符被一视同仁。汉字不再使用“两个扩展ASCII”,而是使用“1个Unicode”,注意,现在的汉字是“一个字符”了,于是,拆字、统计字数这些问题也就自然而然的解决了。但是,这个世界不是理想的,不可能在一夜之间所有的系统都使用Unicode来处理字符,所以Unicode在诞生之日,就必须考虑一个严峻的问题:和ASCII字符集之间的不兼容问题。 我们知道,ASCII字符是单个字节的,比如“A”的ASCII是65。而Unicode是双字节的,比如“A”的Unicode是0065,这就造成了一个非常大的问题:以前处理ASCII的那套机制不能被用来处理Unicode了。另一个更加严重的问题是,C语言使用’\0’作为字符串结尾,而Unicode里恰恰有很多字符都有一个字节为0,这样一来,C语言的字符串函数将无法正常处理Unicode,除非把世界上所有用C写的程序以及他们所用的函数库全部换掉。
于是,比Unicode更伟大的东东诞生了,之所以说它更伟大是因为它让Unicode不再存在于纸上,而是真实的存在于我们大家的电脑中。那就是:UTF。UTF= UCS Transformation Format,即UCS转换(传输)格式。它是将Unicode编码规则和计算机的实际编码对应起来的一个规则。现在流行的UTF有2种:UTF-8和UTF-16.
3.UTF-8
编码规则
- 单字节符号:字节的第一位设为0,后面7位为这个符号的码点.对于英文字母来说,UTF-8和ASCII 码是相同的,即兼容了ASCII码.
- 对于 n 字节的符号(n>1),第一个字节的前n位都设为1,第n+1位设为0,后面字节的前两位一律设为10,剩下的没有提及的二进制位,全部为这个符号的码点.
- 详细的看 维基百科,解释的通俗易懂.
- UTF-8 不使用大尾序和小尾序
UTF-8 编码字节的含义:
- 对于UTF-8编码中的任意字节B,如果B的第一位为0,则B独立的表示一个字符(ASCII码);
- 如果B的第一位为1,第二位为0,则B为一个多字节字符中的一个字节(非ASCII字符);
- 如果B的前两位为1,第三位为0,则B为两个字节表示的字符中的第一个字节;
- 如果B的前三位为1,第四位为0,则B为三个字节表示的字符中的第一个字节;
- 如果B的前四位为1,第五位为0,则B为四个字节表示的字符中的第一个字节;
- 因此,对UTF-8编码中的任意字节,根据第一位,可判断是否为ASCII字符;根据前二位,可判断该字节是否为一个字符编码的第一个字节;根据前四位(如果前两位均为1),可确定该字节为字符编码的第一个字节,并且可判断对应的字符由几个字节表示;根据前五位(如果前四位为1),可判断编码是否有错误或数据传输过程中是否有错误。
示例演示
- 严的 Unicode 是4E25(0100 1110 0010 0101),
- 根据上表,可以发现4E25处在第三行的范围内(0000 0800 - 0000 FFFF),因此严的 UTF-8 编码需要三个字节,即格式是1110 xxxx 10xx xxxx 10xx xxxx。
- 然后,从严的最后一个二进制位开始,依次从后向前填入格式中的x,多出的位补0。
- 这样就得到了,严的 UTF-8 编码是1110 0100, 1011 1000, 1010 0101,转换成十六进制就是 E4B8A5
- 还可以参考: https://segmentfault.com/a/1190000018756296
注意:
-
中文在 UTF-8 中并不一定长 3 个字节
非常见字可能占用 4-6 字节.
3字节对应的 unicode 为 <= FFFF
UTF8编码中,英文字符占用一个字节;
绝大多数汉字占用三个字节,个别汉字占用四个字节
下面有示例代码
-
UTF-8 一般使用 1-4 字节为每个字符编码.
-
理论上UTF-8 最多需要用 6 字节表示一个字符.
//https://www.qqxiuzi.cn/zh/hanzi-unicode-bianma.php
//根据上面的网站可以找到 4字节 的 utf-8 汉字
public static void main(String[] args) {
String[] chineses={"𠀂","𤭢"};
//使用UTF-8编码方式进行编码。
try {
byte[] byte0 = chineses[0].getBytes("utf-8");
byte[] byte1 = chineses[1].getBytes("utf-8");
System.out.println(Arrays.toString(byte0));
//[-16, -96, -128, -126]
System.out.println(Arrays.toString(byte1));
//[-16, -92, -83, -94]
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
4. UTF-16
- Unicode的编码空间从U+0000到U+10FFFF,共有1,112,064个码位(code point)可用来映射字符。
- 具体规则参考维基百科
- 使用 2/ 4 个字节表示
转换规则: 参考链接
//U+22222(大于U+10000) 进行转码UTF16
1)先进行减去0x10000
0x22222 - 0x10000 = 0x12222 = 1 0010 0010 0010 0010
2)转换二进制并且分割位高低10位
二进制1111111111 = 1023十进制
2.1)利用按位与的特性获取低10位
二进制1111111111 & 1 00100010 00100010 = 10 0010 0010
十进制1023 & 0x12222 = 546(10进制,就是上面的二进制)
2.2)利用带符号右移运算符以及按位与获取高10位
0x12222 << 10 = 72 = 1001000
1001000 & 1111111111 = 1001000
72 & 1023 = 72
3)
3.1)高10位加0xD800
72 + 0xD800 = 55368 = 0xd848
3.2)低10位加0xDC00
546 + 0xDC00 = 56866 = 0xde22
U+22222编码转UTF-16 = [0xd848,0xde22]
var str = "";
[0xd848,0xde22].forEach(item => {
str +=String.fromCharCode(item)
})
console.log(str);
2.5 其他的编码方式
MIME:MIME 是“多用途网际邮件扩充协议”的缩写,在 MIME 协议之前,邮件的编码曾经有过 UUENCODE 等编码方式 ,但是由于 MIME 协议算法简单,并且易于扩展,现在已经成为邮件编码方式的主流,不仅是用来传输 8 bit 的字符,也可以用来传送二进制的文件 ,如邮件附件中的图像、音频等信息,而且扩展了很多基于MIME 的应用。从编码方式来说,MIME 定义了两种编码方法Base64与QP(Quote-Printable)
3. UTF 的字节序和 BOM
3.1 字节序
UTF-8以字节为编码单元,没有字节序的问题。UTF-16以两个字节为编码单元,在解释一个UTF-16文本前,首先要弄清楚每个编码单元的字节序。例如收到一个“奎”的Unicode编码是594E,“乙”的Unicode编码是4E59。如果我们收到UTF-16字节流“594E”,那么这是“奎”还是“乙”?
Unicode规范中推荐的标记字节顺序的方法是BOM(Byte Order Mark).
- 在UCS编码中有一个叫做"ZERO WIDTH NO-BREAK SPACE"的字符,它的编码是FEFF。而FFFE在UCS中是不存在的字符,所以不应该出现在实际传输中。
- UCS规范建议我们在传输字节流前,先传输字符"ZERO WIDTH NO-BREAK SPACE"。
- 如果接收者收到FEFF,就表明这个字节流是Big-Endian的;
- 如果收到FFFE,就表明这个字节流是Little-Endian的。
- 因此字符"ZERO WIDTH NO-BREAK SPACE"又被称作BOM。
UTF-8不需要BOM来表明字节顺序,但可以用BOM来表明编码方式。字符"ZERO WIDTH NO-BREAK SPACE"的UTF-8编码是EF BB BF(读者可以用我们前面介绍的编码方法验证一下)。所以如果接收者收到以EF BB BF开头的字节流,就知道这是UTF-8编码了。 (不会验证)
一个特殊的空格ZERO WIDTH NO-BREAK SPACE
编码方式 | BOM |
---|---|
UTF-8 | EE BB FF |
UTF-16(BE) | FE FF |
UTF-16(LE) | FF FE |
3.2 BOM
为了识别 Unicode 文件,Microsoft 建议所有的 Unicode 文件应该以 ZERO WIDTH NOBREAK SPACE(U+FEFF)字符开头。这作为一个“特征符”或“字节顺序标记(byte-order mark,BOM)”来识别文件中使用的编码和字节顺序。
BOM 与 XML: XML 解析 XML 文档时的规则:
- 文档中有 BOM,则定义了文件的编码
- 无 BOM,就查看 XML 声明 中的编码属性
- 都没有,假定文档采用 UTF-8 编码
4. 误区
4.1 容易产生的误解
- Java 中,字符串类 java.lang.String 处理的是 UNICODE 字符串,不是 ANSI 字符串。我们只需要把字符串作为“抽象的符号的串”来看待。因此不存在字符串的内码的问题。
4.2 网页提交字符串
当页面中的表单提交字符串时:
- 首先把字符串按照当前页面的编码,转化成字节串。
- 然后再将每个字节转化成 “%XX” 的格式提交到 Web 服务器。比如,一个编码为 GB2312 的页面,提交 “中” 这个字符串时,提交给服务器的内容为 “%D6%D0”。
在服务器端:
- Web 服务器把收到的 “%D6%D0” 转化成 [0xD6, 0xD0] 两个字节,
- 然后再根据 GB2312 编码规则得到 “中” 字。
在 Tomcat 服务器中,request.getParameter() 得到乱码时
- 常常是因为前面提到的“误解一”造成的。
- 默认情况下,当提交 “%D6%D0” 给 Tomcat 服务器时,request.getParameter() 将返回 [0x00D6, 0x00D0] 两个 UNICODE 字符,而不是返回一个 “中” 字符。
- 因此,我们需要使用 bytes = string.getBytes(“iso-8859-1”) 得到原始的字节串,再用 string = new String(bytes, “GB2312”) 重新得到正确的字符串 “中”。
4.3 从数据库读取字符串
通过数据库客户端(比如 ODBC 或 JDBC)从数据库服务器中读取字符串时,客户端需要从服务器获知所使用的 ANSI 编码。当数据库服务器发送字节流给客户端时,客户端负责将字节流按照正确的编码转化成 UNICODE 字符串。
如果从数据库读取字符串时得到乱码,而数据库中存放的数据又是正确的,那么往往还是因为前面提到的“误解一”造成的。解决的办法还是通过 string = new String( string.getBytes(“iso-8859-1”), “GB2312”) 的方法,重新得到原始的字节串,再重新使用正确的编码转化成字符串。
4.4 char 能不能存中文
//char 内部使用的是 utf-16 编码,2字节能够保存常见的中文(绝大部分)
//char charA= '𠀂';
char charB = '严';
//char t = '𤭢';
// utf-16 编码为: D852 DF62, 两个字节,char 无法存
4.5 解码编码
解码:一串二进制数,使用一种编码方式,转换成字符,这个过程我们称之为解码.使用正确的解码方式才能显示正确的文字,否则就会出现乱码.
编码:一串已经解码后的字符,我们也可以选用任意类型的编码方式重新转换成一串二进制数,这个过程就是编码.
如果编码规范的字库表不包含目标字符,则无法在字符集中找到对应的二进制数。这将导致不可逆的乱码!
例如:像 ISO-8859-1的字库表中不包含中文,因此哪怕将中文字符使用ISO-8859-1进行编码,再使用ISO-8859-1进行解码,也无法显示出正确的中文字符。
// 使用 ISO-8859-1 字符集中不存在的字符进行编码
//中文全部被编码成了 63
public static void main(String[] args) {
String str = "程序员";
byte[] bytes = null;
try {
bytes = str.getBytes("ISO-8859-1");
String s1 = new String(bytes, "ISO-8859-1");
System.out.println(s1); //???
System.out.println(Arrays.toString(bytes));
//[63, 63, 63]
System.out.println("***************");
bytes = str.getBytes("GBK");
String s = new String(bytes, "GBK");
System.out.println(s); //程序员
System.out.println(Arrays.toString(bytes));
//[-77, -52, -48, -14, -44, -79]
//使用什么方式编码,就是用什么方式解码
String gbk = new String(bytes, "utf-8");
System.out.println(gbk); // ����Ա 乱码了
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}