计算机中最小的存储单元是byte

字符 A 的存取:

  • 存入:A 存入硬盘,编码为 0100 0001(65)
  • 取出:0100 0001 取出,解码为A

为了更方便的查看二进制数据,我们引入了16进制。每四位二进制转化为1位16进制。

IDEA默认使用Unicode的UTF-8编码(英文1字节,中文3字节),Eclipse默认使用GBK编码(英文1字节,中文2字节)

常见的编码表

ASCII

American Standard Code for Information Interchange,美国信息互换标准代码。128个字符用7位刚好可用表示,但计算机中存储的最小单元是byte,ASCII码中最高位设置为0,剩下7位表示字符。这7位可以看作数组0到127,ASCII码规定了从0到127个的每个数字代表什么含义。如下图所示,除了中文之外,我们平常用的字符基本都涵盖了,键盘上的字符大部分也都涵盖了。

0 – 127一共128个字符,7位刚好涵盖。

数字32到126表示的这些字符都是可打印字符,0到31和127表示一些不可以打印的字符,这些字符一般用于控制目的,这些字符中大部分都是不常用的,下表列出了其中相对常用的字符。

//打印水平制表符,ASCII码表中是9  
char c = 9;  
System.out.print(c);  
System.out.println("hello world");  
//  hello world

ASCII码对美国是够用了,但对别的国家而言却是不够的,于是,各个国家的各种计算机厂商就发明了各种各样的编码方式以表示自己国家的字符,为了保持与ASCII码的兼容性,一般都是将最高位设置为1。也就是说,当最高位为0时,表示Ascii码,当为1时就是各个国家自己的字符。在这些扩展的编码中,在西欧国家中流行的是ISO 8859-1和Windows-1252,在中国是GB2312,GBK,GB18030和Big5,我们逐个来研究这些编码。

西欧编码 ISO-8859-1

  • 一个字节表示一个字符

ISO 8859-1又称Latin-1,它也是使用一个字节表示一个字符,因为西欧的文字也都是字母拼接,只不过不是26个英文字母罢了,其中0到127与ASCII一样,128到255规定了不同的含义。在128到255中,128到159表示一些控制字符,这些字符也不常用,就不介绍了。160到255表示一些西欧字符,如下图所示:

127是 0111 1111,128是 1000 0000,也就是从128开始就表示西欧的文字和控制字符

西欧编码 windows-1252

ISO 8859-1虽然号称是标准,用于西欧国家,但它连欧元() 这个符号都没有,因为欧元比较晚,而标准比较早。实际使用中更为广泛的是Windows-1252编码,这个编码与ISO8859-1基本是一样的,区别 只在于数字128到159,Windows-1252使用其中的一些数字表示可打印字符,这些数字表示的含义,如下图所示:

而Latin-1中128-159表示的是不可打印的控制字符

这个编码中加入了欧元符号以及一些其他常用的字符。基本上可以认为,ISO 8859-1已被Windows-1252取代,在很多应用程序中,即使文件声明它采用的是ISO 8859-1编码,解析的时候依然被当做Windows-1252编码。

HTML5 甚至明确规定,如果文件声明的是ISO 8859-1编码,它应该被看做Windows-1252编码。为什么要这样呢?因为大部分人搞不清楚ISO 8859-1和Windows-1252的区别,当他说ISO 8859-1的时候,其实他实际指的是Windows-1252,所以标准干脆就这么强制了。

中文编码 GB2312

  • 两个字节表示一个中文字符

美国和西欧字符用一个字节就够了,但中文显然是不够的。中文第一个标准是GB2312。GB2312标准主要针对的是简体中文常见字符,包括约7000个汉字,不包括一些罕见字,不包括繁体字。GB2312固定使用两个字节表示汉字,在这两个字节中,最高位都是1,如果是0,就认为是Ascii字符。在这两个字节中,其中第一个字节范围是1010 0001(十进制161) – 1111 0111(十进制247),第二个字节范围是1010 0001(十进制161) – 1111 1110(十进制254)。 比如,”贤哥”的GB2312编码是

一共的范围 = 第一个字节范围 87 * 第二个字节范围 94 = 8198

CF, CD B8 , E7
  • 最高位为0,按照ASCII解析
  • 最高位为1,按照GB2312解析两个字节(都必须为1)。

为了方便的查看二进制 和 十进制 和 十六进制的转换 ,可以使用下面的两个方法。

/**
格式化打印:0b1111 -> 二进制: 1111  十进制: 15  十六进制: F
 */
private static void printFormatFromBinary(int binary) {
    System.out.println("二进制: "+Integer.toBinaryString(binary)+"  十进制: "+binary+"  十六进制: "+Integer.toHexString(binary).toUpperCase());
}

/**
格式化打印:0xFF -> 二进制: 11111111  十进制: 255  十六进制: F
*/
private static void printFormatFromHex(int hex) {
    System.out.println("二进制: "+Integer.toBinaryString(hex)+"  十进制: "+hex+"  十六进制: "+Integer.toHexString(hex).toUpperCase());
}

中文编码 GBK

23940

  • 两个字节表示一个中文字符

GBK建立在GB2312的基础上,向下兼容GB2312,也就是说,GB2312编码的字符的二进制表示,在GBK编码里是完全一样的。GBK增加了一万四千多个汉字,共计约21000汉字,其中包括繁体字。

GBK同样使用固定的两个字节表示,其中第一个字节范围是1000 0001(十进制129) – 1111 1110(十进制254),第二个字节范围是0100 0000(十进制64) – 0111 1110(十进制126)和1000 0000(十进制128) – 1111 1110(十进制254)。 需要注意的是,第二个字节是从64开始的(64属于byte正数范围,和ASCII的编码重合了),也就是说,第二个字节最高位可能为0。那怎么知道它是汉字的一部分,还是一个ASCII字符呢? ​

其实很简单,因为汉字是用固定两个字节表示的,在解析二进制流的时候,如果第一个字节的最高位为1,那么就将下一个字节读进来一起解析为一个汉字,而不用考虑它的最高位,解析完后,跳到第三个字节继续解析。

  • 如果第一个字节的最高位为1,下一个字节读进来一起解析为一个汉字,而不用考虑它的最高位。
  • 如果最高位为0,按照ASCII码解析

中文编码 GB18030

  • 变长编码,中文两个字节或四个字节

GB18030向下兼容GBK,增加了五万五千多个字符,共七万六千多个字符。包括了很多少数民族字符,以及中日韩统一字符。

用两个字节已经表示不了GB18030中的所有字符,GB18030使用变长编码,有的字符是两个字节,有的是四个字节。

  • 在两字节编码中,字节表示范围与GBK一样。
  • 在四字节编码中,第一个字节的值从1000 0001(十进制129) 到11111110(十进制254),第二个字节的值从0011 0000(十进制48)到0011 1001(十进制57),第三个字节的值从1000 0001(十进制129) 到11111110(十进制254),第四个字节的值从0011 0000(十进制48)到0011 1001(十进制57)。 ​

解析二进制时,如何知道是两个字节还是四个字节表示一个字符呢?

很简单,看第二个字节的范围,如果是48到57就是四个字节表示,因为两个字节编码中第二字节都比这个大(GBK第二字节最小64)。

综合说明GB18030兼容GBK,兼容GB2312,兼容ASCII,但是GB18030,GBK,GB2312这三个编码和ISO8859-1是不兼容的。

繁体中文编码 Big5

Big5是针对繁体中文的,广泛用于台湾香港等地。Big5包括1万3千多个繁体字,和GB2312类似,一个字符同样固定使用两个字节表示。

在这两个字节中,第一个字节范围是 十进制129 到 十进制254,第二个字节范围是 十进制64 – 十进制126 和 十进制161 – 十进制254。

Big5和GB18030,GBK,GB2312不兼容,如果已经理解了上文,其实就能理解为什么Big5和GB的三个编码为什么不兼容了。

编码表汇总

ASCII码是基础,一个字节表示,最高位设为0,其他7位表示128个字符。其他编码都是兼容Ascii的,最高位使用1来进行区分。

  • 西欧主要使用Windows-1252,使用一个字节,增加了额外128个字符。
  • 中文大陆地区的三个主要编码GB2312,GBK,GB18030,有时间先后关系,表示的字符数越来越多,且后面的兼容前面的,GB2312和GBK都是用两个字节表示,而GB18030则使用两个或四个字节表示(取决于第二个字节的范围)。
  • 香港台湾地区的主要编码是Big5。​
  • 如果文本里的字符都是ASCII码字符,那么采用以上所说的任一编码方式都是一样的,不会乱码。但如果有高位为1的字符,除了GB2312/GBK/GB18030外,其他编码都是不兼容的,比如,Windows-1252和中文的各种编码是不兼容的,即使Big5和GB18030都能表示繁体字,其表示方式也是不一样的,而这就会出现所谓的乱码。

乱码和兼容

兼容:GB2312/GBK/GB18030 ASCII是兼容的 比如我们文本里面 a字符,使用这四种码表任何一种都是可以正常显示的。

windows-1252和ISO-8859-1 和ASCII是兼容的

Big5和ASCII是兼容的

但是 西欧编码 和 Big5 以及 GB系列的编码 他们相互之间是不兼容的,也就是 同样的码值在三种编码表中显示的内容是不一样的。

乱码:如果编码的时候同一种编码表,而解码的时候通过的却是一种不兼容的编码表,则就就会出现乱码现象。

Unicode

以上介绍了中文和西欧的字符与编码,但世界上还有很多的国家的字符,每个国家的各种计算机厂商都对自己常用的字符进行编码,在编码的时候基本忽略了别的国家的字符和编码,甚至忽略了同一国家的其他计算机厂商,这样造成的结果就是,出现了太多的编码,且互相不兼容。

世界上所有的字符能不能统一编码呢?可以,这就是Unicode。

Unicode 做了一件事,就是给世界上所有字符都分配了一个唯一的数字编号,这个编号范围从0x000000到0x10FFFF,包括110多万。但大部分常用字符都 在0x0000到0xFFFF之间,即65536个数字之内(2byte)。每个字符都有一个Unicode编号,这个编号一般写成16进制,在前面加U+。大部分中文 的编号范围在U+4E00到U+9FA5,例如,”贤”的Unicode是U+8D24。

Unicode就做了一件事,就是给所有字符分配了唯一数字编号。它并没有规定这个编号怎么对应到二进制表示,这是与上面介绍的其他编码不同的,其他编码都既规定了能表示哪些字符,又规定了每个字符对应的二进制是什么,而Unicode本身只规定了每个字符的数字编号是多少。

贤 -> 在Unicode编码表中找到贤的编号U+8d24,根据 转换方式 UTF-8/UTF-16/UTF-32转换为二进制存储到计算机中

如果规定怎么存储到计算机中,最大值0x10FFFF需要三个字节,最小值也会翻译为三个字节,太浪费空间,所以定义了三种规范。

1990年开始研发,1994年正式公布。随着计算机工作能力的增强,Unicode也在面世以来的十多年里得到普及。
Unicode6.3版已发布(2013年11月)。在Unicode联盟网站上可以查看完整的6.3的核心规范。
Unicode定义了大到足以代表人类所有可读字符的字符集。
Unicode其实应该是一个码值表。Unicode的功用是为每一个字符提供一个唯一的数字码,而对数字码的存储规则的定义则需要依靠UTF-8/UTF-16/UTF-32
UTF-8/UTF-16/UTF-32是通过对Unicode码值进行对应规则转换后,编码保持到内存/文件中。UTF-8/UTF-16都是可变长度的编码方式。

UTF-32

就是字符编号的整数二进制形式,四个字节。

但有个细节,就是字节的排列顺序,如果第一个字节是整数二进制中的最高位,最后一个字节是整数二进制中的最低位,那这种字节序就叫“大端”(Big Endian, BE),否则,正好相反的情况,就叫“小端”(Little Endian, LE)。

对应的编码方式分别是UTF-32BE和UTF-32LE。比如:

Unicode编码 UTF32-LE UTF32-BE
0x006C49 49 6C 00 00 00 00 6C 49
0x020C30 30 0C 02 00 00 02 0C 30

注意:之所以有大端和小端两种方式,是因为硬件读写顺序的不同。

大端:数据的高字节保存在内存的低地址中,低字节保存到内存的高地址中,和我们的阅读习惯一致;小端则相反,常用的X86结构是小端模式。采用大端方式进行数据存放符合人类的正常思维,而采用小端方式进行数据存放利于计算机处理。

可以看出,每个字符都用四个字节表示,非常浪费空间,实际采用的也比较少。

注意:UTF-32太浪费空间。UTF-32是因为UTF-16编码方式不能表示全部的字符而扩充的编码方式

UTF-16

在了解 UTF-16 编码方式之前,先了解一下另外一个概念——”平面”。

在上面的介绍中,提到了 Unicode 是一本很厚的字典,她将全世界所有的字符定义在一个集合里。这么多的字符不是一次性定义的,而是分区定义。每个区可以存放 65536 个(2^16)字符,称为一个平面(plane)。目前,一共有 17 个(2^5)平面(65536 * 17 = 1,114,112‬ 也就是110多万),也就是说,整个 Unicode 字符集的大小现在是 2^21。21个bit位就可以表示所有数字编号。

最前面的 65536 个字符位(0-65535),称为基本平面(简称 BMP),它的码点范围是从 0 到 2^16-1,写成 16 进制就是从 U+0000 到 U+FFFF(65535)。所有最常见的字符都放在这个平面,这是 Unicode 最先定义和公布的一个平面。剩下的字符都放在辅助平面(简称 SMP ),码点范围从 U+010000(FFFF + 1)到 U+10FFFF。

基本了解了平面的概念后,再说回到 UTF-16。UTF-16 编码介于 UTF-32 与 UTF-8 之间,同时结合了定长和变长两种编码方法的特点。它的编码规则很简单:基本平面的字符占用 2 个字节,辅助平面的字符占用 4 个字节。也就是说,UTF-16 的编码长度要么是 2 个字节(U+0000 到 U+FFFF,也就是),要么是 4 个字节(U+010000 到 U+10FFFF)。那么问题来了,当我们遇到两个字节时,到底是把这两个字节当作一个字符还是与后面的两个字节一起当作一个字符呢?

为了将两个字节的UTF-16编码与四个字节的UTF-16编码区分开来,Unicode编码的设计者将0xD800-0xDFFF(基本平面)保留下来,并称为代理区(Surrogate),代理区内的字符四个字节,代理区外2两个字节:

辅助平面的字符位共有 2^20 个(去除基本平面),因此表示这些字符至少需要 20 个bite位,也就是20个bit位就可以表示辅助平面的所有内容。UTF-16 将这 20 个二进制位分成两半,前 10 位映射在 U+D800 到 U+DBFF,称为高代理位(H),后 10 位映射在 U+DC00(U+DBFF + 1) 到 U+DFFF,称为低代理位(L)。这意味着,一个辅助平面的字符,被拆成两个基本平面的字符表示。

D800-DB7F High Surrogates 高位替代
DC00-DFFF Low Surrogates 低位替代

如果U≥0x10000(辅助平面字符),我们先计算U’=U-0x10000(减去辅助平面最低位,不减是21位才能表示),然后将U’写成二进制形式:yyyy yyyy yyxx xxxx xxxx(前十位和后十位),U的UTF-16编码(二进制)就是:

110110yyyyyyyyyy 110111xxxxxxxxxx。前面就会落在高代理区,后面就会落在低代理区。

按照上述规则,Unicode编码0x10000-0x10FFFF(辅助平面)的UTF-16编码有四个字节,前两个字节的高6位是110110,后两个字节的高6位是110111。

可见,前两个字节的取值范围(二进制)是11011000 00000000到11011011 11111111,即0xD800-0xDBFF。

后两个字节取值范围(二进制)是11011100 00000000到11011111 11111111,即0xDC00-0xDFFF。

  • 高代理位前补110110即可映射到高位替代 0xD800-0xDBFF
  • 低代理位前补110111即可映射到低位替代 0xDC00-0xDFFF

因此,当我们遇到两个字节,发现它的码点在 U+D800 到 U+DBFF(高代理位)之间,就可以断定,紧跟在后面的两个字节的码点,应该在 U+DC00 到 U+DFFF(低代理位)之间,这四个字节必须放在一起解读。

接下来,以汉字”𠮷”为例,说明 UTF-16 编码方式是如何工作的。(辅助平面)

汉字”𠮷”的 Unicode 码点为 0x20BB7,该码点显然超出了基本平面的范围(0x0000 – 0xFFFF),因此需要使用四个字节表示。

首先用 0x20BB7 - 0x10000 计算出超出的部分,然后将其用 20 个二进制位表示(不足前面补 0 )

结果为0001000010 1110110111

接着,将前 10 位映射到 U+D800 到 U+DBFF 之间,后 10 位映射到 U+DC00 到 U+DFFF 即可。

U+D800 对应的二进制数为 1101100000000000,直接填充后面的 10 个二进制位即可,得到 1101100001000010,转成 16 进制数则为 0xD842

同理可得,低位为 0xDFB7。因此得出汉字”𠮷”的 UTF-16 编码为 0xD842 0xDFB7

和UTF-32一样,UTF-16也有UTF-16LE和UTF-16BE之分,例如:

Unicode编码 UTF-16LE UTF-16BE UTF32-LE UTF32-BE
0x006C49(基本平面) 49 6C 6C 49 49 6C 00 00 00 00 6C 49
0x020C30(辅助平面) 30 DC 43 D8 D8 43 DC 30 30 0C 02 00 00 02 0C 30

注意:UTF-16常用于系统内部编码,我们平常说的 “Unicode编码是2个字节” 这句话,其实是因为windows系统默认的Unicode编码就是UTF-16,在常用基本字符上2个字节的编码方式已经够用导致的误解,其实是可变长度的。在没有特殊说明的情况下,常说的Unicode编码可以理解为UTF-16编码,而且是UTF-16BE编码。

UTF-16比UTF-32节省了很多空间,但是任何一个字符都至少需要两个字节表示,对于美国和西欧国家而言,还是很浪费的。

UTF-8

UTF-8就是使用变长字节表示,每个字符使用的字节个数与其Unicode编号的大小有关,编号小的使用的字节就少,编号大的使用的字节就多,使用的字节个数从1到4个不等。

具体来说,各个Unicode编号范围对应的二进制格式如下表所示

Unicode编码(十六进制) UTF-8 字节流(二进制)
000000-00007F(0~127) 0xxxxxxx
000080-0007FF(128~2047) 110xxxxx 10xxxxxx
000800-00FFFF(2048~65535 基本面) 1110xxxx 10xxxxxx 10xxxxxx
010000-10FFFF 11110xxx10xxxxxx10xxxxxx10xxxxxx

汉字大多都在基本面内,三个字节就可以表示。

辅助面需要四个字节

图中的x表示可以用的二进制位,而每个字节开头的1或0是固定的。

小于128的(即0x00-0x7F之间的字符),编码与ASCII码一样,最高位为0。其他编号的第一个字节有特殊含义,最高位有几个连续的1表示一共用几个字节表示,而其他字节都以10开头。4字节模板有21个x,即可以容纳21位二进制数字。Unicode的最大码位0x10FFFF也只有21位。

对于一个Unicode编号,具体怎么编码呢?首先将其看做整数,转化为二进制形式(去掉高位的0),然后将二进制位从右向左依次填入到对应的二进制格式x中,填完后,如果对应的二进制格式还有没填的x,则设为0。

例1:“汉”字的Unicode编码是0x6C49。

0x6C49在0x0800-0xFFFF之间,使用3字节模板:1110xxxx 10xxxxxx 10xxxxxx。

将0x6C49写成二进制是:0110 1100 0100 1001, 用这个比特流依次代替模板中的x

得到:11100110 10110001 10001001,即E6 B1 89。

例2:Unicode编码0x20C30在0x010000-0x10FFFF之间,使用4字节模板:11110xxx 10xxxxxx 10xxxxxx 10xxxxxx。

将0x20C30写成21位二进制数字(不足21位就在前面补0):0 0010 0000 1100 0011 0000,用这个比特流依次代替模板中的x

得到:11110000 10100000 10110000 10110000,即F0 A0 B0 B0。

注意:UTF-8和UTF-32/UTF-16不同的地方是UTF-8是兼容ASCII的(-32和-16不兼容),对大部分中文而言,一个中文字符需要用三个字节表示。UTF-8的优势是网络上数据传输英文字符只需要1个字节,可以节省带宽资源。所以当前大部分的网络应用都使用UTF-8编码,因为网络应用的代码编写全部都是使用的英文编写,占据空间小,网络传输速度快。

BOM

我们通常会看到这样的编码 UTF-8和UTF-8+BOM

比如一个文本软件,在打开一个文件的时候,如何判断这个文件是使用的什么编码呢,该用什么编码进行解码呢?那么就需要通过BOM(Byte Order Mark)来指明了。

Unicode标准建议用BOM(Byte Order Mark)来区分字节序,即在传输字节流前,先传输被作为BOM的字符“零宽无中断空格”。这个字符的编码是FEFF,而反过来的FFFE(UTF-16)和FFFE0000(UTF-32)在Unicode中都是未定义的码位,不应该出现在实际传输中。

UTF编码 Byte Order Mark (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-32BE 00 00 FE FF

注意:UTF-8不需要BOM来表明字节顺序,但可以用BOM来表明文件是UTF-8的编码方式。根据BOM的规则,在一段字节流开始时,如果接收到以下字节,则分别表明了该文本文件的编码。而如果不是以BOM开头,那程序则会以ANSI,也就是系统默认编码读取(简体中文WindowsGBK)。

GBK可以两个字节表示中文,UTF-8采用三字节,UTF-16大部分两字节,少部分三字节;UTF-32全部四字节

乱码的原因和可逆性

乱码原因

乱码产生的根源一般情况下可以归结为三方面即:编码引起的乱码、解码引起的乱码以及缺少某种字体库引起的乱码(这种情况需要用户安装对应的字体库),其中大部分乱码问题是由不合适的解码方式造成的

乱码可逆情况

其中缺少字体,只需要安装对应的字体库即可解决乱码,比如 Windows 系统在 C:\Windows\Fonts 目录下会有安装好的字体库列表。安装字体库比较简单,下载后解压,然后复制到对应系统的 Fonts 目录下。

解码方式和编码方式不一致的情况,只需要让解码方式和编码方式一致即可让乱码恢复。

但是,在打开文件显示乱码后(不合适的解码),再保存文件(不合适的编码),这样就是不可逆的。

乱码不可逆情况

GBK编码不支持这几个字符 “𠮷” “” “” , 如果在一个 GBK编码的文件中,写入 “𠮷” “” “” 这些字符, 那么他们就会变成??, ?对应的码值是3F,这样的情况就没有办法恢复。 因为 “𠮷”的本来的码值 变成了 两个 3F (即两个问号),无论如何也不能恢复过来了。

也就是说,如果采用不合适的方式编码字符,就是不可逆的

Java的char字符

在 Java内部进行字符处理时,采用的都是Unicode,具体编码格式是UTF-16BE。简单回顾一下,UTF-16使用两个或四个字节表示一个字符(基本面和辅助面),Unicode编号范围在65536以内的占两个字节,超出范围的占四个字节,BE (Big Endian)就是先输出高位字节,再输出低位字节,这与整数的内存表示是一致的。

char本质上是一个固定占用两个字节的无符号正整数,这个正整数对应于Unicode编号,用于表示那个Unicode编号对应的字符。

char有多种赋值方式:

//直接赋值  
char c = 'a';  
char c1 = '贤';  
System.out.println(c);  
System.out.println(c1);  
  
//赋值unicode对应的十六进制或十进制  
char c2 = 0xbd24;  
char c3 = 36132;  
System.out.println(c2);  
System.out.println(c3);  
  
//直接使用unicode的码值形式  
char c4 = '\u8d24';  
System.out.println(c4);

由于固定占用两个字节,char只能表示Unicode编号在65536以内的字符,而不能表示超出范围的字符。

那超出范围的字符怎么表示呢?只能使用String类来表示,例如汉字”𠮷”的 Unicode 码点为 0x20BB7,该码点显然超出了65535,所只能用String表示,而当粘贴到代码中时,自动转换为了两个字符”\uD842\uDFB7″

 char c ='味';  
 System.out.println(c);  
 //char c1 = '\uD842\uDFB7';  //高位代理 + 地位代理
 String str = "\uD842\uDFB7";  //此时这个String的byte数组长度为4
 System.out.println(str);

char有多种赋值方式:

 char c = 'A';  
 char c = '马';  
 char c = 39532;  
 char c = 0x9a6c;  
 char c = '\u9a6c';

第1种赋值方式是最常见的,将一个能用ASCII码表示的字符赋给一个字符变量。

第 2种也很常见,但这里是个中文字符,需要注意的是,直接写字符常量的时候应该注意文件的编码,比如说,GBK编码的代码文件按UTF-8打开,字符会变成乱码,所以有的时候为了避免代码中出现的汉字常量乱码 可以使用第5中方式赋值,至于汉字和Unicode的码值转换有很多网站可以做到。比如 百度上搜索 汉字 转换Unicode第一条链接http://www.atool9.com/chinese2unicode.php

第3种是直接将十进制的常量赋给字符,第4种是将16进制常量赋给字符,第5种是按Unicode字符形式。

以上,2,3,4,5都是一样的,本质都是将Unicode编号39532赋给了字符

char 的加减运算就是按其Unicode编号进行运算,一般对字符做加减运算没什么意义,但ASCII码字符是有意义的。比如大小写转换,大写A-Z的编号是 65-90,小写a-z的编号是97-122,正好相差32,所以大写转小写只需加32,而小写转大写只需减32。加减运算的另一个应用是加密和解密,将 字符进行某种可逆的数学运算可以做加解密。

String

编码方法

getBytes()

在[[006-String#public byte[] getBytes()|String的常用方法]]中介绍过该方法

此方法根据java命令运行时参数 file.encoding设置的编码表进行编码的

        String str = "你好";
        byte[] bytes = str.getBytes();
        
        System.out.println(Arrays.toString(bytes)); //[-28, -67, -96, -27, -91, -67]
        printHex("unknown",bytes);// type:unknown:   0xE4 0xBD 0xA0 0xE5 0xA5 0xBD 

打印结果是[-28, -67, -96, -27, -91, -67],很明显2个中文6个字节,应该是采用的UTF-8编码,查看getBytes方法的底层发现


    public byte[] getBytes() {
        return StringCoding.encode(value, 0, value.length);
    }
     //省略部分源码
    static byte[] encode(char[] ca, int off, int len) {
        String csn = Charset.defaultCharset().name(); //默认的编码表
        try {
            // use charset name encode() variant which provides caching.
            return encode(csn, ca, off, len);
        } catch (UnsupportedEncodingException x) {
            warnUnsupportedCharset(csn);
        }
        try {
            return encode("ISO-8859-1", ca, off, len);
        } catch (UnsupportedEncodingException x) {
            // If this code is hit during VM initialization, MessageUtils is
            // the only way we will be able to get any kind of error message.
            MessageUtils.err("ISO-8859-1 charset not available: "
                             + x.toString());
            // If we can not find ISO-8859-1 (a required encoding) then things
            // are seriously wrong with the installation.
            System.exit(1);
            return null;
        }
    }

    public static Charset defaultCharset() {
        if (defaultCharset == null) {
            synchronized (Charset.class) {
                String csn = AccessController.doPrivileged(
                    new GetPropertyAction("file.encoding")); 
                    //找到file.encoding,通过System.getProperty可以获取
                Charset cs = lookup(csn);
                if (cs != null)
                    defaultCharset = cs;
                else
                    defaultCharset = forName("UTF-8");
            }
        }
        return defaultCharset;
    }

通过System.getProperty(“file.encoding”)获取该值:

System.out.println(System.getProperty("file.encoding")); //UTF-8

这是IDEA设置file.encoding = UTF-8,如果是EditPlus,该值会输出为GBK,因为EditPlus使用的是Windows默认的编码方式

通过java命令运行java程序的时候 加上 -Dfile.encoding=编码表 来设置运行程序的字符集。

getBytes有一个重载的方法:

getBytes(String charsetName)

此方法根据指定的编码名称charsetName进行编码

System.out.println(Arrays.toString("你好".getBytes("UTF-8"))); //[-28, -67, -96, -27, -91, -67]  六字节  
System.out.println(Arrays.toString("你好".getBytes("UTF-16BE")));//[79, 96, 89, 125] 四字节

解码方法

String(byte[] code)

String(byte[] code, CharSet charSet)

//通过GB2312编码得到的字节数组  
byte[] bytes = "你好".getBytes("GB2312");  
System.out.println(Arrays.toString(bytes));//[-60, -29, -70, -61] 四字节  
  
//1. 通过GB族字符集解码该字节数组  
String strByGB2312 = new String(bytes, Charset.forName("GB2312"));  
String strByGBK = new String(bytes, Charset.forName("GBK"));  
String strByGB18030 = new String(bytes, Charset.forName("GB18030"));  
  
System.out.println("strByGB2312 = " + strByGB2312); //strByGB2312 = 你好  
System.out.println("strByGBK = " + strByGBK); //strByGBK = 你好  
System.out.println("strByGB18030 = " + strByGB18030); //strByGB18030 = 你好  
//证实了GB族编码表互相兼容  
  
//2. 通过UTF族解码该字节数组  
String strByUTF8 = new String(bytes, Charset.forName("UTF-8"));  
System.out.println("strByUTF8 = " + strByUTF8); //strByUTF8 = ���

乱码情况

可逆情况

//可逆情况:不正确的方式解码  
String str = "你好";  
  
//使用UTF-8编码为字节数组  
byte[] bytes = str.getBytes();  
  
//使用不正确的解码,GB系列就会导致乱码  
System.out.println(new String(bytes,"GBK"));//浣犲ソ

这样没有进行”保存”操作,也就是改变字节数组的内容

不可逆情况

//不可逆情况:使用不合适的解码表进行解码后保存了,保存的二进制数据就是乱码的二进制数据,无法恢复  
String hello = "你好";  
  
//UTF-8编码为字节数组  
byte[] utf8Bytes = str.getBytes();  
  
//GBK解码,得到乱码  (也就是用GBK解码了UTF-8格式编码的文件)
String GBKSolveUTF8Bytes = new String(utf8Bytes, "GBK");  
System.out.println(GBKSolveUTF8Bytes);//浣犲ソ(虽然是乱码,但是在码表中还是有对应的字符)  
  
//编码该乱码字符串,此时就有两种情况  
//1. 该乱码字符串在使用的码表中有对应的字符,也就是编号没有改变  
//   使用该码表(或者是兼容的)进行编码,编号还是不变的  
byte[] bytes2 = GBKSolveUTF8Bytes.getBytes("GB18030");  
System.out.println(new String(bytes2)); //你好  
  
//  使用其他码表进行编码,就会导致乱码字符串变为字节数组时编号改变,浣犲ソ变为UTF-8中的编号  
byte[] bytes3 = GBKSolveUTF8Bytes.getBytes("UTF-8");  
//但是这种情况也是可以恢复的,只需要使用UTF-8解码该字节数组,得到 浣犲ソ  
String s1 = new String(bytes3, "UTF-8");  
//再将"浣犲ソ"字符串使用GBK编码,就得到了原先的字节数组  
byte[] bytes1 = s1.getBytes("GBK");  
//将这个字节数组使用UTF-8转化为字符串,就得到了原先的内容  
System.out.println("原先的内容:" + new String(bytes1));  
  
//2. 该乱码字符串在使用的码表中没有对应的字符,也就是从字节数组转化来的时候就已经将编号改变了  
String s = "\uD842\uDFB7"; //𠮷 的Unicode码值  
byte[] bytes4 = s.getBytes("utf-8");  
//𠮷 在GB2312中没有对应的编号,只能显示?,也就导致编号改变  
String string = new String(bytes4, "GB2312");  
System.out.println(string);//���  
//编号改变后使用任何方式都无法获得原先的数据了  
byte[] bytes5 = string.getBytes("GB2312");  
byte[] bytes6 = string.getBytes("UTF-8");  
System.out.println(new String(bytes5));//???  
System.out.println(new String(bytes6));//���

不可逆的本质:当前进行操作使用的码表不支持这个字符,从根本上改变了码值。

不可逆更改的两种情况:

编码导致的不可逆改变:编码时内容所对应的字符在指定的字符集中不存在,该字符被替换为?(63),此时字节数组发生了不可逆改变


String test = "\uD842\uDFB7";  //𠮷
byte[] bytes7 = test.getBytes("GB2312"); //GB2312中没有 𠮷 ,使用?代替 
System.out.println(Arrays.toString(bytes7));  
System.out.println(new String(bytes7,Charset.forName("GB2312")));//63

解码导致的不可逆改变:解码时字节数组中的编号在指定的字符集中不存在,该内容被替换为�,此时字节数组发生了不可逆改变

String test = "\uD842\uDFB7";  
byte[] bytes7 = test.getBytes("UTF-8");  
System.out.println(new String(bytes7,"GB2312"));

如果解码时内容所对应的字符在指定字符集中存在,只是被替换为其他文字,但是还是可逆的。

如果进行”保存”操作,也就是进行编码操作需要进行讨论:

  • 该乱码中的字符是有编号的字符,例如使用GBK解码UTF-8编码的”你好”,得到”浣犲ソ”,虽然出现乱码,但是”浣犲ソ”在GBK中都有对应的编号,此时如果使用GBK保存,没有改变字节数组的内容,下一次再使用正确的解码方式打开即可;如果使用其他字符集(UTF-8等)保存,”浣犲ソ”在GBK和UTF-8中的编号是不一致的,就会导致字节数组的内容改变;虽然改变了字节数组,但是该字节数组通过UTF-8解码还是可以得到”浣犲ソ”,再将”浣犲ソ”用GBK编码就可以得到原本字节数组中的内容了。

  • 但是,一篇文章可能有成千上万字,例如使用GB18030编写的文章,使用GB2312解码的时候将每两个字节转化为一个中文,而GB18030中很多的中文在GB2312中都是没有的,也就是GB2312解码得到了一个在本身码表中不存在的编号,这是GB2312就会使用其他字符(例如?)显示这个内容,这就导致了字节数组的内容发生了不可逆的改变。

ISO-8859-1的妙处

String test = "你好";  
byte[] bs = test.getBytes("GBK");  
String kjk = new String(bs, "UTF-8");  
System.out.println(kjk);//��� [-17, -65, -67, -17, -65, -67, -17, -65, -67]  
byte[] bss = kjk.getBytes("UTF-8");  
System.out.println(new String(bss,"GBK"));//锟斤拷锟�

之前已经讨论过这种操作,使用GBK编码的内容如果使用UTF-8进行解码,分为两种情况:

  1. 字节数组对应的内容在UTF-8码表中存在,被替换为其他文字,例如:浣犲ソ,这样是可逆的。
  2. 字节数组对应的内容在UTF-8码表中不存在,被替换为�,这就是不可逆的,因为�并不代表原来字节数组中对应的那个编号。

�是UTF-8的一个特殊字符,代表三个字节:[-17, -65, -67]

如果有两个这样的�字节数组就是:[-17, -65, -67,-17, -65, -67]

使用GBK翻译这个数组,每两个字节转化为一个中文:

-17, -65 翻译为 锟
-67, -17 翻译为 斤
-65, -67 翻译为 拷

使用ISO-8859-1就可以完成第一种操作:

String test = "你好";  
byte[] bs = test.getBytes("GBK");  
String kjk = new String(bs, "ISO-8859-1");  
System.out.println(kjk);//ÄãºÃ  
byte[] bss = kjk.getBytes("ISO-8859-1");  
System.out.println(new String(bss,"GBK"));//你好

因为使用GBK方式编码的内容在使用ISO-8859-1解码时都可以得到对应的字符,将这些字符再使用ISO-8859-1编码就可以转化为原先的字节数组,在使用GBK解码这个字节数组就能得到真正的内容了。

ISO-8859-1 每一个字节对应一个字符,0-255都对应了一个字符,使用ISO-8859-1解码内容时,逐字节进行解码,每一个字节都能在码表中找到对应的字符。虽然显示的是乱码,但是没有真正的更改字节数组中的内容,再使用ISO-8859-1进行编码就会还原字节数组

IO-字符流

InputStreamReader乱码

当前有文件a.txt:

中国
																			utf-8

因为使用UTF-8编码,底层是6个字节:[-10,-20,-30,-40,-50,-60,-70]

打开这个文件看到了”中国”,是因为文本编辑器会自动按照UTF-8把这六个字节转换为两个汉字

底层实际上就是六个字节。

InputStreamReader isr = new InputStreamReader(new FileInputStream(path), "UTF-8");  
int ch;  
  
while ((ch = isr.read()) != -1){  
    System.out.println((char) ch);  
}

字符流InputStreamReader是不能读取文件的,是通过字节流FileInputStream来读取的。

字符流实际上就是一个缓冲区:

在缓冲区中对读到的字节进行编码和解码。字符输入流就是将读到的字节翻译为字符

因为在创建字符流的时候规定了解码格式为UTF-8,所以此处就会使用UTF-8进行解码。

文件在创建时使用的是UTF-8,字符输入流也是UTF-8,这就不会出问题,但是如果将输入流的解码格式转换为GBK:

InputStreamReader isr = new InputStreamReader(new FileInputStream(path),"GBK");  
int ch;  
  
while ((ch = isr.read()) != -1){  
    System.out.print((char) ch + " "); //涓  浗 
}

因为UTF-8中按照三个字节存储,一共6个字节,而GBK每两个字节进行解码,此时就会翻译为三个字符,示意图:


所以在使用字符输入流时,应该将解码格式设置为文件的编码格式。

OutputStreamWriter乱码

OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream(onputPath),"UTF-8");  
  
osw.write("中国");  
osw.flush();

写出时将”中国”写到缓冲区,在缓冲区中将”中国”按照编码方式转换为字节数据,flush或缓冲区满时将字节数据写入文件。

在打开b.txt的时候文本编辑器会自动把字节数据翻译为对应的字符。

如果指定用GBK编码文件:

OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream(outputPath),"GBK");  
  
osw.write("中国");  
osw.flush();

在缓冲区中将”中国”按照GBK翻译为4个字节:[-34,-56,-90,-120] 再用字节输出流将这个数组写入文件中,idea打开文件时使用UTF-8方式进行解码

复制文件

字符流复制文本文件的乱码因素

4个因素:

  • 源文件编码
  • Reader缓冲区编码
  • Writer缓冲区编码
  • 目标文件编码

源文件编码和Reader缓冲区编码必须一致,Writer缓冲区编码和目标文件编码必须一致。就不会出现乱码。

经过Reader缓冲区的转化,到程序中就是”内容”了,只需要保证Writer的写出编码格式和打开目标文件时的格式一致即可。

OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream(inputPath),"UTF-8");  
InputStreamReader isr = new InputStreamReader(new FileInputStream(outputPath), "UTF-8");  
int ch;  
while ((ch = isr.read()) != -1){  
    osw.write(ch);     
}  
osw.flush();

很多人看到输入流和输出流格式都是UTF-8就认为不会出现乱码,但重要的不是输入输出流保持一致,而是源文件编码与Reader编码一致,Writer编码与目标文件编码必须一致

以本例来说,假设源文件是GBK编码,会有以下两种情况:

  1. 解码时出现不可逆字符,文件不可逆损坏。
  2. 解码时未出现不可逆字符,在Reader缓冲区的是乱码,但是写出时解码格式与读入时一致,乱码又被还原为源字节数组,文件正常复制。

损坏的情况:

而保证了源文件和Reader一致,目标文件和Writer一致后:

GBK文件以GBK格式进行解码,得到内容,将内容以UTF-8格式编码,打开文件时以UTF-8格式解码,没有问题。

字符流UTF-8 编码复制图片

之前的示例中是使用字符流复制文本文件,如果是复制图片呢?

InputStreamReader isr = new InputStreamReader(new FileInputStream("d:\\1.jpg"),"UTF-8"); 
//1.jpg 是15.3kb  能正常打开 
OutputStreamWriter osw2 = new OutputStreamWriter(new FileOutputStream("d:\\2.jpg"),"UTF-8"); 
//2.jpg 是26.5k 不能正常打开
int ch;
while ((ch=isr.read())!=-1) {
    osw2.write(ch);
}
isr.close();
osw2.close();

本质原因是缓冲区会对读入的字节数据进行解码,而解码的过程很有可能出现不可逆字符(因为图片的字节数据是随机的),不可逆字符会被翻译为三个字节(导致图片变大)这样就无法打开目标文件了。

但是可以使用ISO-8859-1进行图片或文件的复制,因为ISO-8859-1逐字节读取并翻译,每一个字节都可以找到对应的字符,不会出现不可逆的情况。在输出时使用ISO-8859-1进行编码就可以得到原先的数据了。

InputStreamReader isr = new InputStreamReader(new FileInputStream("d:\\1.jpg"),"ISO-8859-1");//1.jpg 是15.3kb  能正常打开 
OutputStreamWriter osw2 = new OutputStreamWriter(new FileOutputStream("d:\\2.jpg"),"ISO-8859-1");//2.jpg 是15.3kb  能正常打开 
int ch;
while ((ch=isr.read())!=-1) {
    osw2.write(ch);
}
isr.close();
osw2.close();

tomcat之所以使用ISO-8859-1编码,就是因为ISO-8859-1使得字符具有可逆性。

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。