一、字符集介绍
ANSI:American National Standards Institute。中文:美国国家标准学会
不同国家的和地区为此制定了不同标准,由此产生了 GB2312、GBK、Big5、Shift_JIS 等各自的编码标准。这些使用 1 至 4 个字节来代表一个字符的各种汉字延伸编码方式,称为 ANSI 编码。在简体中文Windows操作系统中,ANSI 编码代表 GBK 编码;在日文Windows操作系统中,ANSI 编码代表 Shift_JIS 编码。 不同 ANSI 编码之间互不兼容,当信息在国际间交流时,无法将属于两种语言的文字,存储在同一段 ANSI 编码的文本中。
UNICODE:Unicode是国际组织制定的可以容纳世界上所有文字和符号的字符编码方案。Unicode用数字0-0x10FFFF来映射这些字符,最多可以容纳1114112个字符,或者说有1114112个码位。码位就是可以分配给字符的数字。UTF-8、UTF-16、UTF-32都是将数字转换到程序数据的编码方案。
例:
Unicode中文“艺”字: 827A
二进制的“艺”字编码:1000 0010 0111 1010
UTF-8的中文编码规则: 1110xxxx 10xxxxxx 10xxxxxx
UTF-8的“艺”字编码: 1110【1000】 10【0010】【01】 10【11】【1010】
UTF-8的转码过程解析: 8对应的1000被填入第一字节剩余的4位。2对应的0010被填入第2字节剩余的前4位。7对应的0111被拆开,前2位01被填入第2字节的后两位,后2位1被填入第3字节的前2位。A对应的1010被填入第3字节的后4位。
UTF-8的最终编码结果:11101000---对应E8;10001001---对应89;10111010---对应BA。所以最终的UTF-8编码就是%E8%89%BA
Unicode到UTF-8的转换:Unicode的16进制编码<-->对应的2进制编码<-->UTF-8规范的2进制编码<-->UTF-8规范的16进制编码
也就是说假如在Java的底层JVM,由于采用的是Unicode编码字符集,对“艺”字的编码是827A。那么在网络传输的过程中,我们当然不能直接传输827A这个字符过去代表艺”这个汉字,而必须要转换成0,1这样的字节流,才能在网络中传输。
所以说UTF-8是一种为了方便网路传输,节省传输数量,而对Unicode的字符集的字符编号进行转换,从定长的2个字节(16进制)转换成1~3个的变长字节(2进制)表示的转换格式。
由于Unicode采用的是2个字节的编码方式,而UTF-8转换后可能是1~3个字节,所以同一个汉字,在Unicode中的编码和经UTF-8转换后的编码值肯定是不同的。就好像艺字的Unicode编码是827A,经转换后的3个字节是E889BA。
所以说对于英文字符来说,采用UTF-8对Unicode编码转换后节省了一倍的传输成本(由定长的2个字节变长1个字节),但对于原本双字节的东亚字符来说,反而增加了成本,是原来的1.5倍。
小结:
①ASCII、GB2312、GBK、GB18030、Big5、Unicode都是字符集的名称。它们定义了采用1~2个字节的编码规范,为每个字符赋予了一个独一无二的编号。这个编号就是我们所说的“字符编码”。
②Unicode字符集定义的字符编码并不适合直接通过网络传输表达,因为它们必须转换成像0101这样的二进制字节流传输。所以就出现了不同的转换规范实现方式:UTF-8,TF-16等。这些不同的转换规范转换后的编码值和Unicode是不同的。
对于UTF-8来说,它采用变长字节表示所有Unicode字符,对于英文来说和ASCII兼容,对于东亚字符来说,是原来传输成本的1.5倍。所以采用UTF-8编码转换方式虽然有利于统一,但增加了中文等双字节字符的传输成本。
UTF-8采用首字节的高位"1"的个数表示字符的编码长度。例如在Unicode的编码规范中:汉字的表示区间为U-00000800至U-0000FFFF对应的UTF-8的转换规则为:1110xxxx 10xxxxxx 10xxxxxx 首字节3个1代表这个字符的编码长度为3个字节。如果是2个1则表示2个字节
③在底层的平台中如JVM,采用的是Unicode字符集,当要把这些字符通过网络传输时,可以选择通过UTF-8或其他(例如GB2312)编码转换方式对要传输的字符编码进行转换。如果目的端也是采用Unicode字符集,那么UTF-8转换后的编码可以被正常识别并解码成最终对应的Unicode字符集编号。如果是非Unicode字符集平台则可能出现乱码(UTF-8中汉字的3个连续字节被解析成GB2312的2个连续字节,出现丢失)。所以推荐在传输的两端采用Unicode字符集编码,在传输方式上采用UTF-8转换方式。
javac命令是以系统默认编码读入源文件,然后按Unicode进行编码的。(备注:每个文件都有自己的编码,javac命令按照默认的文件编码读入,但是在将.java文件转换成.class的过程中,javac会将所有的字符转化成unicode的格式保存。)
在运行时JVM也是采用unicode编码的,并且默认输入和输出使用的都是操作系统的默认编码。也就是说在new String(bytes[,encode])中,系统认为输入的bytes是编码为encode的字节流(如果不指定encode,那么就是默认使用系统的编码方式),换句话说,如果按encode来翻译bytes才能得到正确的原始字符,这个字符最后要在java中保存,它还是要从这个encode转换成Unicode的。
也就是说,假如我们需要从磁盘文件、数据库记录、网络传输一些字符,保存到Java的变量中,要经历由bytes-->encode字符-->Unicode字符的转换(例如new String(bytes, encode));而要把Java变量保存到文件、数据库或者通过网络传输,系统要做一个Unicode字符-->encode字符-->bytes的转换(例如String.getBytes([encode]))
二、JAVA编码与new String(byte[],charset)
JAVA采用Unicode字符集。
即不管采用什么样的编解码,最终char和String都是Unicode编码。
这里需要理解两个函数
1、 getBytes();
getBytes()、getBytes(encoding)函数的作用是使用系统默认或者指定的字符集编码方式,将字符串编码成字节数组。这里的系统不是windows那个系统的GBK,而是在Eclipse文件右键属性的text file encoding指定的编码,
比如开发环境中设置的编码格式是utf-8,
那么System.getProperty(“file encoding”)=utf-8。
getBytes()默认采用的就是utf-8。
具体的在开发环境中说,下面进入正题。
str.getBytes("utf-8")
以前在servlet中经常用这句话,但是从来都没有去理解过。这个函数的意思是对str这个字符串用iso-8859-1这个字符集进行重新编码并返回byte数组,这里要注意几点:
首先str是Unicode,不管之前加了这个那种charset得到,它都是Unicode字符集编码的。我们知道String本身其实是
char[]的包装类,这个char也是Unicode,双字节编码。这个函数就是对每个Unicode 的char字符采用iso-8859-1这个字符集进行编码,最终得到一个字节数组。
例如:
private final static char[] HEX="0123456789abcdef".toCharArray();
public static void main(String[] args) throws Exception{
String string="艺艺";
byte[] b=string.getBytes("utf-8");
System.out.printf("utf-8 %s%n", bytes2HexString(b));
byte[] c=string.getBytes("unicode");
System.out.printf("unicode %s%n", bytes2HexString(c));
}
public static String bytes2HexString(byte[] bys) {
char[] chs = new char[bys.length * 2 + bys.length - 1];
for(int i = 0, offset = 0; i < bys.length; i++) {
if(i > 0) {
chs[offset++] = ' ';
}
chs[offset++] = HEX[bys[i] >> 4 & 0xf];
chs[offset++] = HEX[bys[i] & 0xf];
}
return new String(chs);
}
输出:
utf-8 e8 89 ba e8 89 ba
unicode fe ff 82 7a 82 7a
默认采用的是utf-8格式,每个中文字符战三个字节,所以上面是6个字节
但是Unicode每个字符占两个字节为什么这里也是6个呢?
这是因为Unicode规范中推荐的标记字节顺序的方法是BOM。BOM不是“Bill Of Material”的BOM表,而是Byte Order Mark。
(Unicode是一种字符编码方法,不过它是由国际组织设计,可以容纳全世界所有语言文字的编码方案。Unicode的学名是"Universal Multiple-Octet Coded Character Set",简称为UCS。UCS可以看作是"Unicode Character Set"的缩写。)
在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。
在 Java 中直接使用Unicode 转码时会按照UTF-16LE 的方式拆分,并加上 BOM。 如果采用 UTF-16 拆分,在 Java 中默认采用带有 BOM 的 UTF-16BE 拆分。 (其实Unicode与UTF-8是完全一样的)
UTF-8 是采用 1~4 个字节来表示 Unicode 字符的,每个 Unicode 的 UTF-8 编码的
第一个字节是有一定范围的,如果读取到某个字节的最高位为 0 那么采用一个字节表
示,如果最高位是两个“1”就采用两个字节表示,最高位是三个“1”采用三个字节表
示,以此类推。多字节表示时,第二个和后面的字节的最高位只能是“10”,也就是说
UTF-8 编码时字符的第一个字节的最高位不可能是“10”。
因此,UTF-8 只能采用 Big-Endian 的 BOM 方式。BOM 头 U+FEFF,UTF-8 编码为 EF BB BF就隐藏掉了。
从上面可以看出这个函数最终实现的是按照charset来得到对应的byte数组
2、new String(byte[],charset);
这个函数是对byte[]按照charset进行编码,假如没有charset就直接采用file encoding进行编码。
比如这个byte[]假如是”GBK”的,要是采用”UTF-8”进行编码肯定是错误的,因为JVM不会自动地对byte[]进行扩展,而是按照”UTF-8”的规则进行编码,这样肯定是会产生乱码的。
所以对于new String(tmp.getBytes("GBK"), "UTF-8") 这个过程,JVM内部是不会帮你自动对字节进行扩展以适应UTF-8的编码的。正确的方法应该是根据UTF-8的编码规则进行字节的扩充,即手动从2个字节变成3个字节,然后再转换成十六进制的UTF-8编码。直接使用肯定得到的是乱码。
一般来说编码最好统一。
另外这句话的意思是什么呢?以前的理解是将这个byte[]数组编码成utf-8编码字符集的字符串,这个字符串就是
utf-8的,这个理解是错的。这个函数是用charset字符集对byte[]进行编码,按照1110,,,等规则(见上面)判断得到字符并转换成Unicode编码的字符串,所以这个函数最终得到的还是Unicode编码的字符串,每个字符占两位。
三、在IO中使用字符集
1、data.json是ANSI字符集,中文windows是GBK,内容是 啊连发16565,Eclipse的环境是UTF_8
读写txt文件时我们会经常使用FileReader
BufferedReader reader=new BufferedReader(new FileReader("d:\data.json"));
StringBuilder builder=new StringBuilder();
String string=reader.readLine();
while(string!=null)
{
builder.append(string);
string=reader.readLine();
}
System.out.println(builder.toString());
reader.close();
直接采用这种方式输出������16565 出现乱码,因为FileReader不能设置编码方式只能使用UTF-8对GBK编码的文件进行读取,肯定出现乱码,出现这种乱码在后面无法还原。
当把data.json换成UTF-8字符集的时候就能够正常的读出来 啊连发16565
为此我们在不知道TXT编码读取文件的时候,最好采用下面的方法。
先做一个简单的测试
File file=new File("d:\data.json");
InputStream stream=new FileInputStream(file);
byte b1=(byte)stream.read();
byte b2=(byte)stream.read();
byte b3=(byte)stream.read();
long len2=file.length();
System.out.println(Integer.toHexString(b1 & 0xff));
System.out.println(Integer.toHexString(b2 & 0xff));
System.out.println(Integer.toHexString(b3 & 0xff));
System.out.println(len2);
stream.close();
这段代码输出的是
ef
bb
bf
17
前三个正是utf-8的BOM(BYTE ORDER MARK)
那么可以采用这种方法来判断文件的编码,由于UNICODE采用的是2个字节,假如文件为空的话将会出现错误。
所以采用下面的方法先判断文件编码格式
private String getCharset(String fileName) throws IOException{
BufferedInputStream bin = new BufferedInputStream(new FileInputStream(fileName));
int p = (bin.read() << 8) + bin.read();
String code = null;
switch (p) {
case 0xefbb:
code = "UTF-8";
break;
case 0xfffe:
code = "Unicode";
break;
case 0xfeff:
code = "UTF-16BE";
break;
default:
code = "GBK";
}
return code;
}
获取字符串采用下面的
public String getTextFromText(String filePath){
try {
InputStreamReader isr = new InputStreamReader(new FileInputStream(filePath),getCharset(filePath));
BufferedReader br = new BufferedReader(isr);
StringBuffer sb = new StringBuffer();
String temp = null;
while((temp = br.readLine()) != null){
sb.append(temp);
}
br.close();
return sb.toString();
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
}
这样就不会乱码啦
四、在J2EE中使用(Request,Response,Tomcat)
浏览器请求会先转到tomcat中 tomcat 配置文件中的默认编码格式 就是 iso-8859-1,所以对于utf-8格式的字符串,我们就采用
new String(temp.getBytes(“iso-8859-1”),”utf-8”);
假如不想这样获取也可以采用下面的方式
Post方式:request.setCharacterEncoding(”utf-8 “);
对于get方式:需在server.xml中的:
<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443"
URIEncoding="utf-8" />设置
输出:(必须在write之前调用)
response.setContentType("text/html;charset=utf-8")是设置页面中为中文编码
五、Eclipse设置
Eclipse有四个地方可以设置字符集
1、 设置整个workspace的字符集,所有的工程默认都会采用这个字符集。
Windows->preference->Text File Ecncoding->UTF-8
2、 在工程上右键
Propertise->resource-> Text File Ecncoding->UTF-8
3、 在源文件上
Propertise->resource-> Text File Ecncoding->UTF-8
注意这里的编码将直接影响到 System.getProperty(“file.encoding”)
public static void main(String[] args) throws Exception{
System.out.println(System.getProperty("file.encoding"));
BufferedReader reader=new BufferedReader(new FileReader("d:\data.json"));
StringBuilder builder=new StringBuilder();
String string=reader.readLine();
while(string!=null)
{
builder.append(string);
string=reader.readLine();
}
System.out.println(builder.toString());
reader.close();
}
这里将文件字符集改成Unicode 并且将text file encoding改成Unicode,那么输出OK,即默认采用的是该文件text file encoding,而不是系统的GBK
UTF-16
啊连发16565
4、Run->run configuration->右边的Common控制的是控制台的输出
六、如何防止乱码
数据库、开发环境、页面编码、Java容器全部统一编码。
如何涉及到socket通信,在传输的时候也要采用相同的字符集