字符集和Java中的字符集
为什么需要引入字符集
- 我们知道,我们的电脑必须要存入我们使用的字符,来表达我们的意思,但是我们知道我们的计算机只能存储0 1组成的二进制编码,而二进制编码可以表示一个数字,所以我们用一个数字来表示一个字符,这个数字就是这个字符的唯一ID,也成为码点
- 这种就叫做我们的字符集
字符集和字符编码的区别
- 即编码字符集,给字符表里的抽象字符编上一个数字,也就是字符集合到一个整数集合的映射。这种映射称为编码字符集,Unicode 字符集就是属于这一层的概念;
- 即字符编码表,根据一定的算法,将编码字符集中字符对应的码点转换成一定长度的二进制序列,以便于计算机处理,这个对应关系被称为字符编码表,UTF-8、 UTF-16 属于这层概念;
- 我们的编码是必须依赖于我们的字符集的,就像我们的接口实现类必须依赖于接口
- 一个字符集可以由我们多个编码方式实现,就像一个接口可以有多个实现类一样
ASCII编码
- 一开始我们的电脑是由我们的美国佬发明的,所以它们也需要将自己需要的字符在电脑中存储了,美国佬只需要一些字母,数字,标点符号和一些控制字符等就够了,所需要的字符数量也不是很多,所以用一个字节存储也就够了
- 一个字节是8位,标准的ASCII有128个字符,所以用一个字节存储就够了,而且我们的第一位必须是0
- 我们编码ASCII码也不需要经过特别的处理,只需要将十进制的码点值转换为对应的二进制
OEM 字符集的衍生
后来,随着计算机在世界各地的流行与发展,人们越发地发现,ASCII 字符集里那可怜的 128 个字符已经不能再满足他们的需求了。
因为这个世界,不是所有人都在用英语,有法语、有德语、有俄罗斯语等等,很多国家用的不是英文,他们的字母里有许多是 ASCII 字符集里没有的。于是这个时候,为了可以在计算机中保存他们各自国家的文字,人们就在想,一个字节能够表示的数字(编号)有 256 个,而 ASCII 字符集只用到了 0x00~0x7F,也就是只占用了前 128 个,后面 128 个数字不用白不用,因此很多人打起了后面这 128 个数字的主意。
它们用 0x7F(127) 之后的空位来表示新的字母、符号,甚至还加入了很多画表格时需要用到的横线、竖线、交叉等字符,一直将 256 个字符全部用完。
但是,同时产生这样的想法并实施行动的,并不只是一个国家,于是乎,又一个问题产生了:不同国家(似乎大部分是欧洲国家,并未验证)的字符集可能不同,即使他们很默契地没有改动前 128 位(用于兼容 ASCII 字符集),但后 128 位也会因为国家的不同、语言的不同而分别对应不同的字符。这就导致了当时世界上出现了大量各式各样的 OEM 字符集。
而在这些字符集中,同一个编码序号表示的字符可能完全不同,例如:144 在阿拉伯人的字符集中是 گ,而在俄罗斯的字符集中却是 ђ。
GB2312
- 但是对于我们中国来说,才100多个字符根本不够用,所以采用了双字节来创建对应的字符表
既然我们说中国使用的是双字节字符集编码(DBCS),那么,到底是怎么使用的呢?
参考了 OEM 字符集的做法,中国也对 ASCII 字符集做了兼容,前 127 号字符保持不变,而从 127 号之后就是我们新加入的字符。
- 规定:一个小于 127 的字符的意义与 ASCII 相同,但两个大于 127 的字符连在一起时,就表示一个汉字,前面的一个字节(高字节)从 0xA1 用到 0xF7,后面一个字节(低字节)从 0xA1 到 0xFE,这样我们就可以组合出大约 7000 多个简体汉字了。
在这些编码里,我们还把数学符号、罗马希腊的字母、日文的假名们都编进去了,连在 ASCII 里本来就有的数字、标点、字母等都统统重新编了两个字节长的编码,这就是常说的“全角”字符,而原来在 127 号以下的那些就叫"半角"字符了。于是就把这种汉字方案叫做 GB2312。
GB2312 是对 ASCII 的中文扩展。
GBK
后来,随着计算机在中国的普及,一个新的问题出现了,并日渐常见:很多人的名字打不出来——一些罕见的字并未收录在 GB2312 中!
解决方法
不再要求低字节一定是 127 号之后的内码,只要第一个字节是大于 127 就固定表示这是一个汉字的开始,不管后面跟的是不是扩展字符集里的内容。
这个办法最终形成了一套新的编码方案,也就是 GBK(汉字内码扩展规范) 字符集。
GBK 不仅包括了 GB2312 的所有内容,同时还增加了近 20000 个新的汉字(包括繁体字)和符号。GB18030
后来,我们又在 GBK 中新加入了数千个少数民族的字符,于是,GBK 便顺势扩展成为了 GB18030。
Unicode 字符集
-
但是还是存在一个致命的问题,就是各个国家使用的字符集都是不一样的,随着全球化和互联网的发展,各个国家直接的通信肯定就是非常的不方便,可能就会出现乱码的问题,所以这时候出现了Unicode(万国码)
-
而且,在 Unicode 标准中,仅仅为每个字符分配了一个唯一的字符编号(代码点,Code Point),对于这个数字对应的二进制串如何存储并没有规定(UTF-8,UTF-16,UTF-32)。
-
值得注意的是:这个数字采用 U+ 紧跟着十六进制数表示。例如:
U+56DE
代表汉字回
。 -
所有字符按照使用上的频繁度划分为 17 个平面(编号为 0-16),即:
-
基本的多语言平面和增补平面(辅助平面)。
-
基本的多语言平面(英文为 Basic Multilingual Plane,简称 BMP)又称平面 0,收集了使用最广泛的字符,代码点从 U+0000 到 U+FFFF,每个平面有 216=65536 个码点;
-
增补平面(辅助平面)从平面 1-16,分为增补多语言平面(平面 1)、增补象形平面(平面 2)、保留平面(平 3-13)、增补专用平面等,每个增补平面也有 216=65536 个码点。
-
所以 17 个平面总计有 17 × 65,536 = 1,114,112 个码点。
-
-
Unicode 字符集的几种常见编码方式
Unicode 字符集中的字符可以有多种不同的编码方式,如 UTF-8、UTF-16、UTF-32、压缩转换等。这里的 UTF 是 Unicode Transformation Format 的缩写,即统一码转换格式,将 Unicode 编码空间中每个码点和字节序列进行一一映射的算法。
- 编码:将字母,数字,图片,符号等转换为不同的比特序列代表不同的字符;
- 解码:将存储在计算机中的比特位序列(或者叫二进制序列)解析显示出来成对应的字母,数字,图片和符号。
UTF-32
UTF-32
是一个以固定四字节编码方式,所有的字符都用四个字节,特别浪费空间,所以实际上使用比较少。- 他的原理很简单,有一串二进制串,我们就认为32位是一个码点
UTF-8
UTF-8是一个可变长的编码方案,一共分为4个长度区:1个字节,2个字节,3个字节,4个字节
- 它是支持ASCII码的的,如果首字节以0开头,表示这个字节是以单字节编码,也就是我们的ASCII值
- 如果是以110开头,说明是以两字节编码
- 如果是也1110开头,说明以三字节编码,我们的汉字就是三字节
- 如果以11110开头,说明是四字节编码
- 对于多字节编码来说,除了首字节,后面的字节都是以10开头编码
UTF-16
UTF-16 编码介于 UTF-8 与 UTF-32 之间,同时结合了定长和变长两种编码方法的特点。它的编码规则很简单:
- 基本多语言平面(BMP)中有效码点用固定两字节的 16 位代码单元为其编码,其数值等于相应的码点,桶 USC-2 的编码方式;
- 辅助多语言平面 1-16 中的有效码点采用代理对(surrogate pair)对其编码:用两个基本平面中未定义字符的码点合起来表示增补平面中的码点编码。
也就是说,UTF-16 的编码长度要么是 2 个字节(U+0000 到 U+FFFF),要么是 4 个字节(U+010000 到 U+10FFFF)。那么问题来了,当我们遇到两个字节时,到底是把这两个字节当作一个字符还是与后面的两个字节一起当作一个字符呢?
这里有一个很巧妙的地方,在基本平面内,从 U+D800 到 U+DFFF(十进制 55296-57343,共 2048 个码点) 是一个空段,即这些码点不对应任何字符。于是,这个空段被用来作为代理区(Surrogate Zone),用两个基本平面中的代理区(代理对,surrogate pair)代理辅助多语言平面 1-16 中的一个有效码点。
表示辅助平面的字符需要20位。UTF-16 将这 20 个二进制位分成两半,前 10 位映射在 U+D800 到 U+DBFF,称为高代理码点,后 10 位映射在 U+DC00 到 U+DFFF,称为低代理码点。
因此,当遇到两个字节,发现它的码点在 U+D800 到 U+DBFF 之间,就可以断定,紧跟在后面的两个字节的码点,应该在 U+DC00 到 U+DFFF 之间,这四个字节必须放在一起解读,表示一个辅助平面的字符。
Unicode和char的联系
- Java 在设计之初便选择了 Unicode 字符集,同时选择的编码方案为 UTF-16!
- 这样的设计在最初是非常优秀的,既满足了可以表示世界上所有语言(几乎)的需求,,Java 的基础类型之一 char 类型被设计为 2 个字节,用于表示一个字符!
- 但是,随着时间的推移,Unicode 字符集不可避免地进行了一次又一次的扩充,很快便超过了 65536(216) 个字符。于是,这个时候,一个 char 表示不了所有的字符了!于是,Java 做出了一些改变,用一个 char 表示 Unicode 的一个代码单元 codePoint(是指一个已编码的文本中具有最短的比特组合的单元;在 UTF-16 中即 16 个比特位)!
- 采取这样的方案后,一个字符便既有可能是用 1 个 char 来表示,也有可能是用 2 个 char 来表示!具体一点来说,如果一个字符位于 Unicode 的基本语言平面,那么用 1 个char 就可以表示它;但如果该字符位于辅助语言平面,那么就需要用 2 个 char 才能表示它。
总结
- Unicode 字符集仅规定了每个字符的码点(唯一编码ID)
- Unicode 字符集有多种编码方案:UTF-8、UTF-16、UTF-32等
- Java 使用的字符集为 Unicode 字符集,编码方案为 UTF-16
- Java 中的 char 占 2 个字节
- Java 中 char 代表 UTF-16 编码方案下 Unicode 编码的一个代码单元(Code Unit)
- Java 中一个字符可能占 1 个 char,也可能占 2 个 char
- 引用别人一句话强烈建议不要在程序中使用 char 类型,如果可以,请使用 String,因为我得到一个char类型,他的类型是一个代码单元,所以可能不是一个完整的Unicode字符
Java8 String和Uncoide
- 因为Java8 底层的String用的是一个char数组来存储
- 我们的String的length()得到是代码单元的个数,如果想让辅助语言面的字符算一个字符,那么要使用codePointCount
- charAt()得到的也是对应第几个代码单元
- 因为我们的UTF-16对于存储ASCII值也是用两个字节,所以我们的Java9底层用的是byte[]数组
- 外部使用来看,都是UTF-16编码,只是内部进行转换来达到优化的功能
Java代码一套编码使用情况
- 对于我们的源文件编码可以由我们手动设置
- 对于编译时候的编码,通过javac 来编译时,可以来确定的
- 字节码编码和JVM内存编码都是确定不可改变的
- 输出流编码
- 对于System.out默认是JVM的编码,也可以通过JVM参数修改
- 对于HTTP响应流由开发人员指定
- 显示编码
- 控制台 通常是系统的默认编码
- 浏览器:自动检测,比如Content-Type
- 一般出现乱码是因为1和2 或者 5和6不兼容