注:在上一篇涉及到Windows和C语言关于字符编码的问题,我们来看一下Java对于字符编码的处理方式;
至于UTF-8.UTF-16,UTF-32的含义,不再重复;Unicode是一个标准,如何存储这个信息,是编码方案处理的方式:UTF就是其中一种。
为了更好的兼容语言和跨平台,Java String 保存的就是字符的Unicode码
它以前使用UCS-2编码方案来存储Unicode,后来发现BMP范围内字符不够用
但是出于内存类型和兼容性的考虑,并没有提升到UCS-4(固定4字节编码)
而是采用了UTF-16
实践:char类型可以看作是16bits字节,如果是在BMP范围内,2字节可以保存的就没有问题。若是BMP之外的字符,他的计算就会有问题。
length()返回的就是16bits作为单位的数量,而不是实际字符的个数。
charAt()返回的也自然就是16bits作为单位;
java编译的时候还是不会处理 0xFFFF之外的Unicode字面量变量;
所以你输入法无法直接打印出来的,知道Unicode,你可以手动计算出来UTF-16(4字节)
把前后两个字节都单独作为一个Unicode,一块赋值给String即可
例子:
Unicode:0x1D11E
查询计算得到UTF-16:变成两个Unicode
D834 DD1E ----" \uD834\uDD1E" 就可以了
注:---------超出0xFFFF------------
按照规则,
Unicode编码0x10000-0x10FFFF的UTF-16编码有两个WORD,
第一个WORD的高6位是110110,
第二个WORD的高6位是110111。
可见,
第一个WORD的取值范围(二进制)是11011000 00000000到11011011 11111111,即0xD800-0xDBFF。
第二个WORD的取值范围(二进制)是11011100 00000000到11011111 11111111,即0xDC00-0xDFFF。
上面所说的从U+D800到U+DFFF的码位(代理区),
就是为了将一个WORD(2字节)的UTF-16编码与两个WORD的UTF-16编码区分开来。
再回到Windows下,记事本中有一个另存为Unicode编码,还是有点歧义。其实是按照UTF-16的方案来保存。
在BMP范围内的UTF-16编码值和Unicode数值是相等的。
可以了解一下高位优先(Unicode big endian)
问题:世界上还存在很多非Unicode的字符,那是另一套编码标准。
毕竟Unicode的空间需求更大,比ANSI大,所以还是有必要存在的
可以建立一个转换平台,将其他标准的字符统一处理成Unicode
WIndows内置函数,全是自动转换为Unicode,所以建议直接使用Unicode,否则会调用这些可能存在bug的转换程序,耗时耗力。
这个转换其实就是维护了不同的编码标准的映射关系,微软称之为Code Page
cp936 是 GBK的Code Page;cp65001是UTF-8的Code Page
应该也有反向映射表 Unicode->GBK
基本流程:
GBK转UTF-8.
第一步,在GBK的编码标准中,找到GBK字符编码,
第二步,在GBK的Code Page 中查到Unicode数值‘
第三步,根据Unicode计算或者查UTF-8的Code Page得到UTF-8的编码实现
注:UTF-8是8bits的,对重要的一步就是第一步,得到正确的Unicode;
这就是转码丢失问题的本质:Code Page的选择有问题
例子:JSP是使用ISO-8859-1去解码HTTP请求参数,就是来使用这一种Code Page
new String(s.getBytes("iso-8859-1"),"UTF-8");
第一步就是根据iso-8859-1找到Unicode数值,然后转为UTF-8的方案
ISO-8859-1是按照字节编码的,不同于ASCII,ISO-8859-1对于0~255空间的每一位都进行了编码,所以任意一个字节都能在它的Code Page找到对应的Unicode数值;
因为有了Unicode,相互之间转化都是没有丢失的;这就是ISO-8859-1的思路注:欧美程序员直接使用JSP解码好的String,其他国家的语言,需要转化为原始字节流,再用对应的Code Page找到解码自己的符号
Java String
构造方法:就是将各种编码数据转为Unicode数值,但是使用UTF-16的编码方案存储,想获得Unicode,就需要对String 进行UTF-16解码即可
//GBK(大陆使用)编码 你好
byte[] gbkData = {
(byte) 0xc4,
(byte) 0xe3,
(byte) 0xba,
(byte) 0xc3
};
//BIG5(台湾使用)编码 你好
byte[] big5Data = {
(byte) 0xa7,
(byte) 0x41,
(byte) 0xa6,
(byte) 0x6e
};
//String 构造就是将不同编码数据转为Unicode
//使用GBK Code Page 来查找对应字符的Unicode;
String str1 = new String(gbkData,"GBK");
//使用GBK Code Page 来查找对应字符的Unicode;
String str2 = new String(big5Data,"BIG5");
//查看Unicode是否一样?
showUnicode(str1);
showUnicode(str2);
}
public void showUnicode(String str){
for(int i=0;i<str.length();i++){
System.out.printf("\\u%x",(int)str.charAt(i));
}
System.out.println();
}
结果如下截图
自然一定是一样的,有错误,只有一个可能:Code Page需要重新维护了
Java String
构造方法:就是将各种编码数据转为Unicode数值;
而问题的关键就是Code Page,需要针对你传递过来的编码数值,指定对字符集,使用该字符集的Code Page,就一定可以解码出来正确一致的Unicode数值。
问题:如果知道Unicode数值,使用String怎么转换到其他编码,而不是UTF-16?
Ans:根据你指定的字符集,就回去Code Page查询Unicode对应的该字符集的编码值。如果该字符集没有该Unicode,这个是很常见,毕竟其他的字符集都比较小。那么就会转换失败;而且转成啥,谁也无法确定。那自然就永久丢失Unicode数值。无法复原
Unicode是一个桥梁,编码之间先找到这个值,然后使用Code Page查找对应的关系
例子:
GBK转为BIG5
第一步:拿到GBK的字符编码:byte[]
byte[] gbkData = {
(byte) 0xc4,
(byte) 0xe3,
(byte) 0xba,
(byte) 0xc3
};
第二步:根据Code Page查找Unicode数值:\u的数值
String str1 = new String(gbkData,"GBK");
第三步:将标准的Unicode通过目标编码集的Code Page找到对应的编码数值:byte[]
byte[] testbig5 = str1.getBytes("big5");
//打印测试一下
System.out.println(new String(testbig5,"big5"));
// 也可以用16进制打印一下
public void showBytes(byte[] data){
for (byte b:data){
// 0x 16进制
System.out.printf("0x%x\n",b);
}
System.out.println();
}
问题:ISO-8859-1有啥特殊的地方,解码比较好
ans:它本身的库还是比较大的。能够查到很多Unicode值;
第一步:拿到某个编码的数据byte
byte[] data = {***,***,***};
第二步:使用ISO-8859-1的Code Page解码
String IsoStr = new String(data,"ISO-8859-1");
第三步:如果你拿着ISO-8859-1解码好的数值,直接显示出现问题,
System.out.println(ISOStr);
那么就可以
第四步:反查ISO-8859-1的Code Page
byte[] UnicodeData = ISOStr.getBytes("ISO-8859-1");
第五步:打印一下byte[],你会发现它还原到data了
showBytes(UnicodeData);
public void showBytes(byte[] data){
for (byte b:data){
// 0x 16进制
System.out.printf("0x%x\n",b);
}
System.out.println();
}
第六步:不使用ISO-8859-1,询问数据来源处,数据的编码是啥,使用对应的字符集解码
如果不知道,那就解不了;但是原始数据还在;
这就是ISO-8859-1的特殊之处。
也就是即使得到了错误的Unicode,通过ISO-8859-1也能反查到之前的原始数据,再选择别的重新解码
对比:
ASCII 虽然也是每个字节对应一个字符,但是它只对0~127进行了编码
也就是每个字节最大为0x7F ;当字节0xe4这样大于0x7F,ASCII就找不到对应的字节;(也就是Code Page 查不到字符对应的Unicode)
针对这种情况,Java确定了一个默认值,找不到就使用字符?,也就是
\u fffd
当Unicode反查的时候也找不到时,就是用默认值 0x3f;
所以就可以解释java的一些输出结果了
注:尽量别使用Windows记事本,因为它会在UTF-8文件最前面加一个3bits的BOM头
很多程序不兼容这个情况
但是在Windows环境中编译,尤其是默认环境:简体中文,GBK字符集解码
你就会看到GBK 在Code Page 有很多字符找不到对应的Unicode
经典报错:编码GBK的不可映射字符
就是查不到Code Page对应关系嘛
追加:单个中文编译失败,偶数个中文就会成功
ANS:Java String内部使用时Unicode,编译的时候就会对
字面量值进行转码,数字不要,字符串需要。
从源文件转换为Unicode
没有指定encoding,就会编译器默认使用GBK解码
UTF8:一个中文一般需要三个字节去编码(8bits*3=24bits)
GBK: 一个中文一般需要两个字节去编码(8bits*2=16bits)
所以,奇偶性会影响结果:
简单来说,如果两个字符,UTF-8需要6个字节,以GBK方式,6个字节按照顺序,可以解码三个字符;
如果是1个字符,UTF-8是3个字节。以GBK解码,那么就多出来一个字节,Java就会用默认值?
例子:
中国两个字符的
UTF-8的编码方案是 e4 b8 ad e5 9b bd
这个编码顺序被GBK解码后是 6d93 e15e 6d57(这三个字符一定是别的字符)
经过GBK解码后,编译后,这3个Unicode会在class文件中
其中JVM中使用的是Unicode,输出是根据字符集转换传递给客户端
这个字符集就是系统区域设置的编码,也就是属于本地化。
这三个Unicode 中 e15e 没有对应的字符。所以不同平台不同字体下会显示不同。
拓展:GBK编码保存的字符,然后使用UTF-8解码,基本上都无法通过编译。
因为UTF-8的编码很有规律,随意组合的字节是不会符合UTF-8的编码规则。
Code Page自然找不到合理的映射。
注:如果想要编译器正确转换为Unicode,必须正确直接告诉编译器源文件的编码。(老老实实的,小动作不要)