目录
一 编码种类
1.1 ASCII码
1.2 ISO8859-1编码
1.3 GBK编码
1.4 Unicode字符集
1.4.1 UTF-8编码
1.4.2 UTF-16编码
1.4.3 UTF-32编码
二 Java编码方式
2.1 内码、外码
2.2 单个char类型能存储大部分(BMP范围内)中文
2.3 String:字符个数和char数组长度的误区
2.4getBytes()方法的转换流程
2.5new String(字节数组,编码方式)方法的转换流程
2.6System.out.println
一 编码种类
先了解下市面上的编码方式的相关知识点,为后面学习打好基础。
1.1 ASCII码
上个世纪60年代,美国制定了一套字符编码,对英文字符与二进制位之间的关系,做了统一规定,这被称为ASCII码,一直沿用至今。
ASCII码 采用单字节编码,一共规定了128个字符的编码,其编码范围是 [0x00 ,0x7F],这128个符号(包括了33个不能打印出来的控制字符或通信专用字符等),只占用了一个字节的后面7位,最前面的1位统一规定为0。
0-31、127是控制字符或通信专用字符,如换行、回车、删除等。32-126是打印字符,可以通过键盘输入并且能够显示出来。
比如空格"SPACE"是32(二进制:0010 0000),大写的字母A是65(二进制:0100 0001)。
ASCII 的编码表可以参考ASCII_百度百科
用128个符号编码便可以表示所有英文字符,但是用来表示其他语言,128个符号是不够的。比如在法语中,字母上方有注音符号,它就无法用 ASCII码 表示。于是,一些欧洲国家就决定,利用字节中闲置的最高位编入新的符号,例如ISO-8859-1编码,可以表示最多256个符号。
但是这里又出现了新的问题,不同的国家有不同的字母。因此,哪怕它们都使用256个符号的编码方式,代表的字母却不一样。比如,130在法语的编码中代表了é,在希伯来语的编码中却代表了字母Gimel (ג),在俄语的编码中又会代表另一个符号。
但是不管怎样,所有这些编码方式中,[0 ,127]表示的符号是一样的,都是英文字符,不一样的只是[128 ,255]的这一段。
1.2 ISO8859-1编码
ISO-8859-1编码采用单字节编码,向下兼容ASCII码,利用字节中闲置的最高位编入新的符号,其编码范围是 [0x00 ,0xFF],[0x00 ,0x7F]的编码和 ASCII码 一致,[0x80 ,0x9F]是控制字符,[0xA0 ,0xFF]是文字符号,此字符集支持部分于欧洲使用的语言。
由于ISO-8859-1 是单字节编码,和计算机最基础的表示单位一致,所以很多时候,计算机默认使用 ISO8859-1编码 来表示,而且在很多协议上,如果不指定编码方式,默认使用该编码。
1.3 GBK编码
GBK编码 采用双字节编码,于1995年制定,兼容标准 ASCII码(低8位是 ASCII码,高8位是0)、GB2312、GB13000-1、BIG5 编码中的所有汉字,编码空间为 [0x8140 ,0xFEFE],共有 23940 个码位,其中 GBK1 区和 GBK2 区也是 GB2312 的编码范围,收录了 21003 个汉字。
1.4 Unicode字符集
世界上存在着多种的编码方式,同一个二进制数可以被解释成不同的符号。因此,要想正确地打开一个文本文件,就必须知道它的编码方式,否则用错误的编码方式解读,就会出现乱码。为什么电子邮件常常出现乱码?就是因为发信人和收信人使用的编码方式不一样。
可以想象,如果有一种编码,将世界上所有的符号都纳入其中,每一个符号都给予一个独一无二的编码的字符集,那么乱码问题就会消失,这就是Unicode,也叫做万国码、统一码,从它的名字就可以看出,这是一种所有符号都有一个独一无二的编码,其编码范围是 [U+000000 ,U+10FFFF]。
需要注意的是,Unicode 只是一个字符集,它只规定了符号的二进制代码,却没有规定这个二进制代码应该如何存储。
比如,汉字“严”的 Unicode代码点 是十六进制0x4E25,转换成二进制数足足有15(100 1110 0010 0101),也就是说这个符号的表示至少需要2个字节,而其他更大的符号,可能需要3个字节或4个字节,甚至更多。
这里就有两个严重的问题:
计算机怎么知道三个字节表示一个符号,而不是分别表示三个符号。我们已经知道英文字符只用一个字节表示就够了,如果 Unicode 统一规定每个符号都用三个或四个字节表示,那么每个英文字符都必然有二到三个字节是0,这对于存储来说是极大的浪费,文本文件的大小会因此大出二三倍,这是无法接受的。
造成的结果是:
出现了 Unicode 的多种编码方式来存储,也就是说有多种不同的二进制编码格式,可以用来表示 Unicode,例如UTF-8、UTF-16、UTF-32。Unicode 在很长一段时间内无法推广,直到互联网的出现。
1.4.1 UTF-8编码
UTF-8编码方式采用可变长字节编码,是一个非常惊艳的编码方式,漂亮地实现了对 ASCII码 的向后兼容,以保证 Unicode 可以被大众接受。UTF-8 是目前互联网上使用最广泛的一种 Unicode编码方式。
它的最大特点就是可变长字节,可使用1-4个字节表示一个字符,根据字符的不同变换字节的个数。
编码规则如下:
对于单个字节的字符,第一位为0,后面的 7 位对应这个字符的 Unicode代码点,与 ASCII码 完全相同,这意味着编码方式为 ASCII码 的文档可以用 UTF-8编码 打开完全没有问题。对于需要使用 N(N > 1)个字节来表示的字符,第一个字节的前 N 位都为1,第 N + 1 位为0,剩余的 N - 1 个字节的前两位都为10,剩下的二进制位则从这个字符的 Unicode代码点 的最后一位开始,从后向前依次填充对应格式中的x,剩下的x用0补上。
例如:"汉"的 Unicode代码点 是 0x6c49(110 1100 0100 1001),通过上面的对照表可以发现,0x6c49位于第三行的范围,那么得出其格式为 1110xxxx 10xxxxxx 10xxxxxx。接着,从"汉"的 Unicode代码点的最后一位开始,从后向前依次填充对应格式中的x,剩下的x用0补上。这样,就得到了"汉"的 UTF-8编码 为 11100110 10110001 10001001,转换成十六进制就是 0xE6 B7 89。
解码的过程:如果字节的第一位是0,则说明这个字节对应一个字符;如果一个字节的第一位是1,那么连续有多少个 1,就表示该字符占用多少个字节。
注意点:在解码的过程如果发现字节的格式不符合UTF-8的编码规则,则会用置换字符(UTF-8编码 的置换字符是�)替换掉不符合的字节。
例如:字节的二进制数的开头为1000 0000,对照 UTF-8 表,发现不对,UTF-8 码表规则不允许用10开头,针对这种情况,转换规则里存在一种机制,会把不允许的字节全部自动变成一个叫"置换字符"的东西,UTF-8编码 的置换字符为�,所以通过 UTF-8 解码得到的字符为�。
1.4.2 UTF-16编码
在了解 UTF-16编码之前,先了解一下 Unicode 中的“平面”概念。
Unicode 是一本很厚的字典,它将全世界所有的字符定义在一个集合里,这么多的字符不是一次性定义的,而是分区定义,每个区可以存放 65536 个(2^16)字符,称为一个平面(plane)。目前,一共有 17 个(2^5)平面,也就是说,整个 Unicode 字符集的大小现在是 2^21。
最前面的 65536 个字符位,称为基本平面(简称 BMP ),它的代码点范围是[0 ,2^16-1],写成 16 进制就是[U+0000 ,U+FFFF]。所有最常见的字符都放在这个平面,这是 Unicode 最先定义和公布的一个平面。
剩下的字符都放在辅助平面或扩展平面(简称 SMP ),码点范围是[U+010000 ,U+10FFFF]。
了解了平面的概念后,再说回到 UTF-16。UTF-16 编码介于 UTF-8与 UTF-32 之间,同时结合了变长和定长字节两种编码方法的特点。
它的编码规则很简单:
基本平面(BMP)内的字符占用2个字节,直接存Unicode代码点。辅助平面(SMP)内的字符占用4个字节,通过转换计算,被拆成两个基本平面的 Unicode代码点(范围是[0xD800 ,0xDFFF],这个范围的 Unicode 不表示任何字符)表示。
也就是说,UTF-16 的编码长度要么是2个字节([U+0000 ,U+FFFF]),要么是4个字节([U+010000 ,U+10FFFF])。
在基本平面(BMP)内,[U+D800 ,U+DFFF]是一个空段,即这些代码点不表示任何字符。因此,这个空段可以用来映射辅助平面的字符。
辅助平面的字符位共有2^20个,因此表示这些字符至少需要20个二进制位。UTF-16 将这20个二进制位分成两半,前10位映射在[U+D800 ,U+DBFF]范围内,称为高位(H),后10位映射在[U+DC00 ,U+DFFF],称为低位(L)。这意味着,一个辅助平面的字符,被拆成两个基本平面的 Unicode代码点表示。
因此,当我们遇到两个字节,发现它的代码点在[U+D800 ,U+DBFF]之间,如果编码正确,则可以断定,紧跟在后面的两个字节的代码点在[U+DC00 ,U+DFFF]之间,这四个字节表示一个字符,必须放在一起解读。
以汉字"?"为例,说明 UTF-16编码 是如何工作的。
汉字"?"的 Unicode代码点 为0x20BB7,该码点超出基本平面的范围[0x0000 ,0xFFFF],在SMP范围内,因此需要使用四个字节表示。首先用 0x20BB7 - 0x10000 计算出超出的部分,然后将其结果用20个二进制位表示(不足前面补0),结果为 0001 0000 1011 1011 0111。接着,将前10位映射到 [U+D800 ,U+DBFF]之间,后10位映射到[U+DC00 ,U+DFFF]即可。U+D800 对应的二进制数为 1101 1000 0000 0000,直接填充后面的 10 个二进制位即可,得到 1101 1000 0100 0010,转成 16 进制数则为 0xD842。同理可得,低位为 0xDFB7。因此得出汉字"?"的 UTF-16编码 为 0xD8 42 DF B7,共4个字节。
Unicode3.0 中给出了辅助平面字符的转换公式:
H = Math.floor((c-0x10000) / 0x400) + 0xD800L = (c - 0x10000) % 0x400 + 0xDC00
UTF-16 用两个字节表示一个字符,那么用两个字节表示必然存在字节序BOM(Byte Order Mark)的问题,即大端小端(大端小端的详情可参考Java中的大端和小端-云海天教程)的问题。
UTF-16根据BOM分为
UTF-16BE(内存低地址端存放高位字节,编码前会放置FEFF)UTF-16LE(内存高地址端存放高位字节,编码前会放置FFFE)
Java采用的 UTF-16BE,也就是大端模式。
(数组:0是低地址端)
1.4.3 UTF-32编码
UTF-32编码采用定长字节编码,对每个字符统一都使用4个字节表示。就空间而言,是非常没有效率的,特别地,非基本平面(BMP)的字符在大部分文件中通常很罕见,以致于它们通常被认为不存在占用空间的大小,使得 UTF-32编码 通常会是其它编码的二到四倍。虽然每一个码位使用固定长的字节看似方便,它并不如其它 Unicode编码方式 使用得广泛。
二 Java编码方式
2.1 内码、外码
Java编码分为内码,外码2种。
内码 :Java程序在JVM中运行时,字符(char、String)在内存中的编码方式,编码方式为UTF-16BE。
也就是说,从外界进入Java世界的字符、字符串数据,无论原本的编码是什么,都会根据原本的编码和 Unicode字符集 之间的映射表,找到原本的编码对应的 Unicode代码点,然后使用 UTF-16 的编码方式存储到内存中,最后输出到外界时,根据外界指定的编码方式和 Unicode字符集 之间的映射表,找到Unicode代码点 映射的外界的编码输出。
外码 :除了内码,皆是外码。注意源代码编译产⽣的目标代码文件(可执行文件或class文件)的编码方式也属于外码。
Javac在编译的时候,会解析Java的源文件从而形成语法树,这个解析的过程是解析Java源文件中的一个个字面的字符(import、public、private、int等)。Java的源文件可以是任意的编码方式,就像普通的文本文件一样,而在经过编译时,先根据源文件的编码方式得到编码,然后根据编码去映射表找到所有字符的Unicode代码点,最后根据 Unicode代码点 生成编码格式为UTF-8 (一种modified UTF-8)的class文件。
在这个过程中存在两种字符集之间的映射规则,如果搞错了编码方式,就会编译失败或者乱码。
例如源文件是 UTF-8编码方式,然而Javac指定ASCII码编码,就有可能超过了 ASCII码 的范围(0-127)而导致编译失败,或者根据编码得到的ASCII编码 和 Unicode 映射得到错误的 Unicode代码点 而乱码,本来是3个字节对应一个 Unicode代码点,使用 ASCII编码 变成 一个字节对应一个 Unicode代码点。
所以在编译期要防止编译错误和乱码的情况发生,就得指定正确的编码来读取源文件,得到正确的编码结果,可使用-encoding参数来指定编码方式。
2.2 单个char类型能存储大部分(BMP范围)中文
为什么中文字符的 UTF-8编码 的长度是3个字节,而char是2个字节也能存中文字符?
之前没了解过Java的编码方式,所以对明明是2个字节的char类型也能存长度为3个字节的中文字符感到很疑惑,而通过查阅资料并总结(可通过阅读上文)后,知道了为什么char也能存中文字符。
答案:因为Java内码的编码方式为 UTF-16,所以Java程序在运行时,并不是将中文字符的 UTF-8 编码 直接存到内存中,而是根据中文字符的 UTF-8编码 去 Unicode 映射表里找到对应的 Unicode代码点,然后根据 Unicode代码点 以 UTF-16 的编码方式存储到内存中。UTF-16 的编码规则通过上文可知,如果中文字符(大部分常见的中文字符都在[U+4E00 ,U+9FBB]范围内,也就是在基本平面内)是在基本平面(BMP)内,则中文字符对应的 UTF-16编码 的长度是2个字节,而char的长度为2个字节,可以存储基本平面(BMP)范围内的所有字符,所以char能存BMP内的中文字符,如果中文字符是在SMP内,则需要2个char才能存储。
这也就是为什么char类型能存储长度为3个字节的 UTF-8编码 的中文字符(注意点:这里的中文字符是指在基本平面内的字符)。
String的内部采用char数组存储字符,可以存储2或4个字节的字符,所以能存储所有的字符。
2.3 String:字符的个数不等于char数组的长度
误区:在没了解Java的编码方式前,由于常见字符都在基本平面BMP内,而不是在SMP内,所以通常来说,String的length()方法的结果(char数组的长度)和字符的个数一样,然后就认为字符串有n个字符,char数组的长度就是n,一个char就能存所有的字符。
正确的理解:由于Java内码的编码方式是 UTF-16,所以char数组的长度是由字符串的字符的 UTF-16编码 决定的。
例如:字符串的字符是在SMP内,就需要2个char才能存储,所以char数组的长度为2,而不是1。
扩展点:1、在Idea开发环境中,如果字符是在辅助平面SMP的范围内,则在Java代码中,Idea会自动将字符根据 UTF-16编码规则 转换成2个长度为2字节的 Unicode代码点。例如:字符""在SMP内,所以在代码里直接被转换显示为"\uD800\uDC00"。
2、在Java中,在字符串中使用以"\u"开头,后面带4个十六进制符号,则表示一个长度为2个字节的 Unicode代码点,范围在[\u0000,\uFFFF]之间。Java运行时,不用通过转换,直接将 Unicode代码点 存到char中,而普通字符则需要通过 UTF16编码 转换成 Unicode代码点 存到char中。
例如:
1、String s = "\uD800\uDC00",赋值给String,String会创建长度为2的char数组存储它。(一个"\uXXXX"长度为2字节的 Unicode代码点 使用一个char存储)
2、String s = "f发\uD9001\uD800\uDFFF";
s的char数组长度为6,如图,注意Debug中显示是字符,实际内存中char存储的是字符的 Unicode代码点。
2.4getBytes()方法的转换流程
Java外码若不指定编码方式,则默认是file.encoding的编码,即操作系统的编码,Idea如果设置开发环境的编码方式为UTF-8,则会自动设置file.encoding的编码为UTF-8。
可参考深入浅出了解Java程序中的乱码 | 码农家园
转换流程:String字符串在调用getBytes(若不指定编码方式,则默认是file.encoding的编码)时,遍历字符串的char数组,根据 UTF-16编码规则 和char存储的 Unicode代码点在Unicode和指定编码方式的映射表找映射行。若找到映射行,则映射行中对应的指定编码方式的编码就是指定编码方式对该字符的编码;若找不到映射行,说明指定的编码方式不存在对该字符的编码,则采用指定的编码方式的置换字符的编码替换该字符。
1、如果遍历到char是在基本平面BMP,且不在[U+D800 , U+DFFF]内,则根据char存储的 Unicode代码点 去映射表找对应映射行。若找到,则映射行中对应的指定编码方式的编码就是指定编码方式对该字符的编码;若找不到映射行,说明指定的编码方式不存在对该字符的编码,则采用指定的编码方式的置换字符的编码替换该字符。
2、如果遍历到char存储的 Unicode代码点 是在[U+D800 , U+DBFF]内。
2.1、继续往后遍历下一个char,如果下一个char存储的 Unicode代码点 在[U+DC00 , U+DFFF]内,则说明这2个char共同表示是一个长度为4字节的字符(在SMP内),将这2个char通过UTF-16编码 的转换公式得到字符的 Unicode代码点,然后根据代码点找到映射行得到编码,若没有,则采用置换字符的编码替换,然后跳过下一个char,遍历下下一个char。
2.2、如果下一个char不是在[U+DC00 , U+DFFF]内或没有下一个char,则可认为该char编码错误(缺少另一个char共同表示长度为4字节的字符)或不表示字符(因为BMP里的[U+D800 , U+DFFF]是一个空段,在这个范围内的 Unicode代码点不表示字符),使用替换字符的编码替换该char,然后继续遍历下一个char。
3、如果遍历到char存储的 Unicode代码点 是在[U+DC00 , U+DFFF]内,则说明该char可能表示的字符不存在或编码错误,缺少另一个char共同表示4字节的字符,使用替换字符的编码替换该char,然后继续遍历下一个char。
Unicode、UTF-8、UTF-16、ISO8859-1 的映射表
以字符串"f发\uD9001"为例,分别采用 ISO8859-1、UTF-8、UTF-16编码方式返回对应编码的字节数组。
a、编码方式:ISO8859-1编码,得到的字节数组流程如下:
1、遍历字符串的char数组,得到第一个char存储的Unicode代码点 0x0066("f"),代码点是在基本平面BMP(排除掉[U+D800 , U+DFFF])里,则根据代码点在映射表里找 ISO8859-1编码 的映射行。发现存在映射行,对应的编码是 0x66,所以字符"f"的 ISO8859-1编码 为 0x66。
2、继续遍历char数组,得到第二个char存储的Unicode代码点 0x53D1("发"),代码点是在基本平面BMP(排除掉[U+D800 , U+DFFF])里,则根据代码点在映射表里找 ISO8859-1编码 的映射行,发现不存在映射行,则采用 ISO8859-1编码 的置换字符"?"的编码 0x3F 替换掉该字符,所以字符"发"的ISO8859-1编码为 0x3F。
3、遍历第三个char:"\uD900",发现char存储的 Unicode代码点 是在[U+D800 , U+DBFF]内,则根据上文的转换流程2,继续遍历下一个char,得到下一个char的 Unicode代码点 0x0031("1") 不在[U+DC00 , U+DBFF]内,所以用 ISO8859-1编码 的替换字符"?"的编码 0x3F 替换掉该字符,所以 Unicode代码点 "\uD900" 的ISO8859-1编码为 0x3F。
4、遍历第四个char存储的Unicode代码点 0x0031("1"),它的 Unicode代码点 是在BMP(排除掉[U+D800 , U+DFFF])里,则根据代码点在映射表里找 ISO8859-1编码 的映射行。发现存在映射行,对应的编码是 0x31,所以字符"1"的 ISO8859-1编码 为 0x31。
5、遍历第五个char:"\uD800",发现存储的 Unicode代码点 是在[U+D800 , U+DBFF]内,则根据上文的转换流程2,继续遍历下一个char,得到下一个char的 Unicode代码点 0xDC00在[U+DC00 , U+DBFF]内,则根据上文的转换流程2.1,这2个char共同表示4字节的""字符,将这2个char通过UTF-16编码 的转换公式得到字符""的 Unicode代码点 \u010000,根据代码点找到映射行得到编码,发现不存在映射行,则采用 ISO8859-1编码 的置换字符"?"的编码 0x3F 替换掉该字符,所以字符""的ISO8859-1编码为 0x3F;然后跳过下一个char,遍历下下一个char。
6、发现没有下下一个char,则遍历结束,最后得到 ISO8859-1编码 的字节数组长度是5,内容为[0x66,0x3F,0x3F,0x31,0x3F],换成十进制(计算机存储的是补码,补码转换成原码,负数取反加1)表示[102,63,63,49,63],如图所示
b、编码方式:UTF-8,得到的字节数组流程如下:
1、遍历字符串的char数组,得到第一个char存储的Unicode代码点 0x0066("f"),代码点是在基本平面BMP(排除掉[U+D800 , U+DFFF])里,则根据代码点在映射表里找 UTF-8编码 的映射行。发现存在映射行,对应的编码是 0x66,所以字符"f"的 UTF-8编码 为 0x66。
2、继续遍历char数组,得到第二个char存储的Unicode代码点 0x53D1("发"),代码点是在基本平面BMP(排除掉[U+D800 , U+DFFF])里,则根据代码点在映射表里找 UTF-8编码 的映射行,发现存在映射行,对应的编码是 0xE5 8F 91,所以字符"发"的 UTF-8编码 为 0xE5 8F 91。
3、遍历第三个char:"\uD900",发现存储的 Unicode代码点 是在[U+D800 , U+DBFF]内,则根据上文的转换流程2,继续遍历下一个char,得到下一个char的 Unicode代码点 0x0031("1") 不在[U+DC00 , U+DBFF]内,所以用 UTF-8编码 的替换字符"?"的编码 0x3F 替换掉该字符,所以 Unicode代码点 "\uD900" 的UTF-8编码为 0x3F。
4、遍历第四个char存储的Unicode代码点 0x0031("1"),它的 Unicode代码点 是在BMP(排除掉[U+D800 , U+DFFF])里,则根据代码点在映射表里找 UTF-8编码 的映射行。发现存在映射行,对应的编码是 0x31,所以字符"1"的 UTF-8编码 为 0x31。
5、遍历第五个char:"\uD800",发现存储的 Unicode代码点 是在[U+D800 , U+DBFF]内,则根据上文的转换流程2,继续遍历下一个char,得到下一个char的 Unicode代码点 0xDC00在[U+DC00 , U+DBFF]内,则根据上文的转换流程2.1,这2个char共同表示4字节的""字符,将这2个char通过UTF-16编码 的转换公式得到字符""的 Unicode代码点 \u010000,根据代码点找到映射行得到编码,发现存在映射行,对应的编码是 0xF0 90 80 80,所以字符""的 UTF-8编码 为 0xF0 90 80 80;然后跳过下一个char,遍历下下一个char。
6、发现没有下下一个char,则遍历结束,最后得到 UTF-8 的字节数组长度是10,内容为[0x66,0xE5,0x8F,0x91,0x3F,0x31,0xF0,0x90,0x80,0x80],换成十进制(计算机存储的是补码,补码转换成原码,负数取反加1)表示[102,-27,-113,-111,63,49,-16,-112,-128,-128],如图所示
c、编码方式:UTF-16,得到的字节数组流程如下:
注意点:UTF16编码 需要指定字节序BOM,Java采用的是大端模式,所以 UTF-16编码 的字节数组会多出2个字节用来表示UTF-16编码 采用大端模式,十六进制表示是 FEFF(十进制:-2,-1),放在字节数组的前2位。
1、遍历字符串的char数组,得到第一个char存储的Unicode代码点 0x0066("f"),代码点是在基本平面BMP(排除掉[U+D800 , U+DFFF])里,则根据代码点在映射表里找 UTF-16编码 的映射行。发现存在映射行,对应的编码是 0x0066,所以字符"f"的 UTF-16编码 为 0x0066。
2、继续遍历char数组,得到第二个char存储的Unicode代码点 0x53D1("发"),代码点是在基本平面BMP(排除掉[U+D800 , U+DFFF])里,则根据代码点在映射表里找 UTF-16编码 的映射行,发现存在映射行,对应的编码是 0x53D1,所以字符"发"的 UTF-16编码 为 0x53D1。
3、遍历第三个char:"\uD900",发现存储的 Unicode代码点 是在[U+D800 , U+DBFF]内,则根据上文的转换流程2,继续遍历下一个char,得到下一个char的 Unicode代码点 0x0031("1") 不在[U+DC00 , U+DBFF]内,所以用 UTF-16编码 的替换字符"�"的编码 0xFFFD替换掉该字符,所以 Unicode代码点 "\uD900" 的UTF-16编码为 0xFFFD。
4、遍历第四个char存储的Unicode代码点 0x0031("1"),它的 Unicode代码点 是在BMP(排除掉[U+D800 , U+DFFF])里,则根据代码点在映射表里找 UTF-16编码 的映射行。发现存在映射行,对应的编码是 0x0031,所以字符"1"的 UTF-16编码 为 0x0031。
5、遍历第五个char:"\uD800",发现存储的 Unicode代码点 是在[U+D800 , U+DBFF]内,则根据上文的转换流程2,继续遍历下一个char,下一个char的 Unicode代码点 0xDC00在[U+DC00 , U+DBFF]内,则根据上文的转换流程2.1,这2个char共同表示4字节的""字符,将这2个char通过UTF-16编码 的转换公式得到字符""的 Unicode代码点 \u010000,根据代码点找到映射行得到编码,发现存在映射行,对应的编码是 0xD800 DC00,所以字符""的 UTF-16编码 为 0xD800DC00;然后跳过下一个char,遍历下下一个char。
6、发现没有下下一个char,则遍历结束,最后得到 UTF-16的字节数组长度是14,内容为[0xFE,0xFF,0x00,0x66,0x53,0xD1,0xFF,0xFD,0x00,0x31,0xD8,0x00,0xDC,0x00],换成十进制(计算机存储的是补码,补码转换成原码,负数取反加1)表示[-2,-1,0,102,83,-47,-1,-3,0,49,-40,0,-36,0],如图所示
代码为
@Testpublic void test20() throws UnsupportedEncodingException {String s = "f发\uD9001\uD800\uDC00";byte[] isoBytes = s.getBytes("ISO8859-1");byte[] utf8Bytes = s.getBytes("UTF-8");byte[] utf16Bytes = s.getBytes("UTF-16");}
2.5new String(字节数组,编码方式)方法的转换流程
new String(bytes, 编码方式)就是将bytes字节数组按照指定的编码方式规则(若是不符合指定编码方式的规则,则采用置换字符的编码替换)得到指定编码方式的编码,然后根据编码去映射表里找到编码的 Unicode代码点,然后按照 UTF-16的编码规则存储到String的char数组里。
转换流程:以字节数组:[0xDF,0xE5,0x8F,0x91]为例,指定编码方式:UTF-8
String utf8Decode = new String(utf8Bytes, "UTF-8");
1、遍历第一个字节 0xDF(-33的补码:1101 1111),发现是 110 开头,根据 UTF-8的编码规则,则继续往后遍历第二个字节 0xE5(1110 0101),发现第二个字节不是以 10 开头的,则说明第一个字节 0xDF 不符合 UTF-8 的编码规则,则采用 UTF-8 的置换字符"�"的编码替换,"�"的 Unicode代码点是 \u00FFFD,在BMP里,直接存到char中,值为 0xFFFD。
2、继续遍历第二个字节 0xE5(-27的补码:1110 0101),发现是 1110 开头,根据 UTF-8的编码规则,则继续往后遍历2个字节,第三个字节是 0x8F(-113的补码:1000 1111),第四个字节是 0x91(-111的补码:1001 0001),都是 10 开头,符合规则,说明这3个字节共同表示一个编码方式 为UTF-8,长度为3字节的字符("发"),将这3个字节组合得到 字符"发"的 UTF-8 的编码,根据映射表,得到字符"发"的 Unicode代码点 为\u0053D1,在BMP里,直接存到char中,值为 0x53D1。跳过第三、四个字节,往后遍历。
3、遍历结束,最后得到的s字符串的char数组的长度为2,分别是[0xFFFD,0x53D1],十进制(char类型显示十进制表示不考虑符号位,符号位参与换算)表示[65533,21457],也就是"�发",如图所示
2.6System.out.println
System.out.println()方法的编码是file.encoding的编码。
如图,在灰色显示为"f发�1",是因为Idea工作环境的编码为 UTF-8,"\uD900"代码点在 Unicode 里不表示字符,所以 UTF-8编码 采用置换字符"�" 替换显示。
而使用System.out.println()输出是"f发?1𐀀",显示"?",而不是显示"�",根据我的猜想,都是不表示字符,而使用的置换字符,System.out.println()内部应该是将字符串s转换为 UTF-8编码 的字节数组,然后将字节数组交由控制台解码输出,而在上文提到,转换为 UTF-8编码的 字节数组,如果是不表示字符,则采用"?"的编码替换,所以控制台才输出"?"。
若是不对,请指教。
参考地址:
java中的编码_Java中的编码_luminousLCH的博客-CSDN博客
Java中如何存储汉字_caoruntao_的博客-CSDN博客_java中汉字用什么类型
Java的字符编码问题一语道破(GBK,UTF-8,ISO-8859-1)
我赌你不懂系列:char占几个字节 - 脉脉
细说Java中的字符和字符串(一)_不去天涯的博客-CSDN博客
彻底弄懂 Unicode 编码_hezh1994的博客-CSDN博客_unicode