Java 字符集总结

java字符集编解码基础介绍,java默认字符集是Unicode编码;可以使用System.out.println(Charset.defaultCharset());查看jvm默认的字符集(我的WIN10为utf-8)

JVM的字符集编码取的是操作系统默认的字符集编码一般情况下:

  • win xp :一般是GBK。
  • WIN 10 :UTF-8
  • Linux 本地语言环境配置文件。

*.java文件可以是GBK、UTF-8等编码,*.class文件统一使用的是Unicode(UTF-16)编码。当程序运行时读取外部的文件,数据库或者接受消息就会在内存中产生非Unicode编码字符串,各种字符串都可以在内存中通过Unicode编码为桥梁进行转换,两种互转换的字符集编码应该足够大,能容纳所要转换的字符串所包含的字符,否则就会出现乱码(比如是UTF-8包含ASCII)。

编码问题存在两个方面:JVM之内和JVM之外

JAVA开发中常见的编码的三种形式

1. 文件编译后形成class

不管在编译前java文件使用何种编码,在编译成class后,他们都Unicode编码表示

2. JVM中的编码

JVM加载class文件读取时候使用Unicode编码方式正确读取class文件,那么原来定义的String s=”汉字”;在内存中的表现形式是Unicode编码。

当调用String.getBytes()的时候,其实已经为乱码买下了祸根。因为此方法使用平台默认的字符集来获取字符串对应的字节数组。在WindowsXP中文版中,使用的默认编码是GBK。

当不同的系统、数据库经过多次编码后,如果对其中的原理不理解,就容易导致乱码。因此,在一个系统中,有必要对字符串的编码做一个统一,这个统一模糊点说,就是对外统一。比如方法字符串参数,IO流,在中文系统中,可以统一使用GBK、GB13080、UTF-8、UTF-16等等都可以,只是要选择有些更大字符集,以保证任何可能用到的字符都可以正常显示,避免乱码的问题。(假设对所有的文件都用ASCII码)那么就无法实现双向转换了。

要特别注意的是,UTF-8并非能容纳了所有的中文字符集编码,因此,在特殊情况下,UTF-8转GB18030可能会出现乱码。

3. 内存中字符串的编码

内存中的字符串不仅仅局限于从class代码中直接加载而来的字符串,还有一些字符串是从文本文件中读取的,还有的是通过数据库读取的,还有可能是从字节数组构建的,然而他们基本上都不是Unicode编码的,原因很简单,存储优化

因此就需要处理各种各样的编码问题,在处理之前,必须明确“源”的编码,然后用指定的编码方式正确读取到内存中。如果是一个方法的参数,实际上必须明确该字符串参数的编码,因为这个参数可能是另外一个日文系统传递过来的。当明确了字符串编码时候,就可以按照要求正确处理字符串,以避免乱码。
在对字符串进行解码编码的时候,应该调用下面的方法:

1
2
getBytes(String charsetName)    
String(byte[] bytes, String charsetName)

而不要使用那些不带字符集名称的方法签名,通过上面两个方法,可以对内存中的字符进行重新编码

字符集应用

JVM 默认字符集—— Charset.defaultCharset ()

Java 中,字符字节转换时,如果不提供字符集,使用默认字符集。例如,字符串和字节数组转换时,字节流和字符流转换时等。

Java 程序入口文件( main 函数所在文件) 编码 —— System.getProperty(“file.encoding”)

该编码默认值取决于 Java 程序入口文件( main 函数所在文件) 编码 的编码方式,具体请参考下面文章,也可以在运行 Java 程序时通过 -Dfile.encoding="GBK" 来设定,如果 -Dfile.encoding 指定的编码方式跟 Java 程序入口文件的字符集不一致,将会导致乱码 。也可以在程序中通过 setP roperty 方法直接设置,这种设置虽然改变了“file.encoding”的值,但是似乎没什么用 。

文件名字编码—— System.getProperty(“sun.jnu.encoding”)

System.getProperty(“sun.jnu.encoding”)

java对字符的处理

在java应用软件中,会有多处涉及到字符集编码,有些地方需要进行正确的设置,有些地方需要进行一定程度的处理。

getBytes(charset)

这是java字符串处理的一个标准函数,其作用是将字符串所表示的字符按照charset编码,并以字节方式表示。注意字符串在java内存中总是按unicode编码存储的。比如”中文”,正常情况下(即没有错误的时候)存储为”4e2d 6587”,如果charset为”gbk”,则被编码为”d6d0 cec4”,然后返回字节”d6 d0 ce c4”。如果charset为”utf8”则最后是”e4 b8 ad e6 96 87”。如果是”iso8859-1”,则由于无法编码,最后返回 “3f 3f”(两个问号)。

new String(charset)

这是java字符串处理的另一个标准函数,和上一个函数的作用相反,将字节数组按照charset编码进行组合识别,最后转换为unicode存储。参考上述getBytes的例子,”gbk” 和”utf8”都可以得出正确的结果”4e2d 6587”,但iso8859-1最后变成了”003f 003f”(两个问号)。

因为utf8可以用来表示/编码所有字符,所以new String(str.getBytes("utf8"),"utf8") == str,即完全可逆。

setCharacterEncoding()

该函数用来设置http请求或者相应的编码。

对于request,是指提交内容的编码,指定后可以通过getParameter()则直接获得正确的字符串,如果不指定,则默认使用iso8859-1编码,需要进一步处理。参见下述”表单输入”。值得注意的是在执行setCharacterEncoding()之前,不能执行任何getParameter()。java doc上说明:This method must be called prior to reading request parameters or reading input using getReader()。而且,该指定只对POST方法有效,对GET方法无效。分析原因,应该是在执行第一个getParameter()的时候,java将会按照编码分析所有的提交内容,而后续的getParameter()不再进行分析,所以setCharacterEncoding()无效。而对于GET方法提交表单是,提交的内容在URL中,一开始就已经按照编码分析所有的提交内容,setCharacterEncoding()自然就无效。

对于response,则是指定输出内容的编码,同时,该设置会传递给浏览器,告诉浏览器输出内容所采用的编码。

处理过程

下面分析两个有代表性的例子,说明java对编码有关问题的处理方法。

表单输入

User input (gbk:d6d0 cec4) browser (gbk:d6d0 cec4) web server iso8859-1(00d6 00d 000ce 00c4) class,需要在class中进行处理:getbytes(“iso8859-1”)为d6 d0 ce c4,new String(“gbk”)为d6d0 cec4,内存中以unicode编码则为4e2d 6587。

  1. 用户输入的编码方式和页面指定的编码有关,也和用户的操作系统有关,所以是不确定的,上例以gbk为例。
  2. 从browser到web server,可以在表单中指定提交内容时使用的字符集,否则会使用页面指定的编码。而如果在url中直接用?的方式输入参数,则其编码往往是操作系统本身的编码,因为这时和页面无关。上述仍旧以gbk编码为例。
  3. Web server接收到的是字节流,默认时(getParameter)会以iso8859-1编码处理之,结果是不正确的,所以需要进行处理。但如果预先设置了编码(通过request. setCharacterEncoding ()),则能够直接获取到正确的结果。
  4. 在页面中指定编码是个好习惯,否则可能失去控制,无法指定正确的编码。

文件编译

假设文件是gbk编码保存的,而编译有两种编码选择:gbk或者iso8859-1,前者是中文windows的默认编码,后者是linux的默认编码,当然也可以在编译时指定编码。

Jsp (gbk:d6d0 cec4) java file (gbk:d6d0 cec4) compiler read uincode(gbk: 4e2d 6587; iso8859-1: 00d6 00d 000ce 00c4) compiler write utf(gbk: e4b8ad e69687; iso8859-1: *) compiled file unicode(gbk: 4e2d 6587; iso8859-1: 00d6 00d 000ce 00c4) class。所以用gbk编码保存,而用iso8859-1编译的结果是不正确的。

class unicode(4e2d 6587) system.out / jsp.out gbk(d6d0 cec4) os console / browser。

  1. 文件可以以多种编码方式保存,中文windows下,默认为ansi/gbk。
  2. 编译器读取文件时,需要得到文件的编码,如果未指定,则使用系统默认编码。一般class文件,是以系统默认编码保存的,所以编译不会出问题,但对于jsp文件,如果在中文windows下编辑保存,而部署在英文linux下运行/编译,则会出现问题。所以需要在jsp文件中用pageEncoding指定编码。
  3. Java编译的时候会转换成统一的unicode编码处理,最后保存的时候再转换为utf编码。
  4. 当系统输出字符的时候,会按指定编码输出,对于中文windows下,System.out将使用gbk编码,而对于response(浏览器),则使用jsp文件头指定的contentType,或者可以直接为response指定编码。同时,会告诉browser网页的编码。如果未指定,则会使用iso8859-1编码。对于中文,应该为browser指定输出字符串的编码。
  5. browser显示网页的时候,首先使用response中指定的编码(jsp文件头指定的contentType最终也反映在response上),如果未指定,则会使用网页中meta项指定中的contentType。

再举开头的例子说明指定编码的必要:
如果一个网页指定编码为utf-8, <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />, 页面上有一个form,提交到一个servlet
那么用户输入的字符传过来的字节流就是按指定编码encoding的,例如你输入了”Hello你好”,如果是utf-8,那么传过来的就是如下:

[104, 101, 108, 108, 111, -28, -67, -96, -27, -91, -67]

我们看到后面汉字每个用了3个字节,这个可以参考Utf-8的相关知识。
但如果你页面指定的是GBK,那传过来的就不一样了:

[104, 101, 108, 108, 111, -60, -29, -70, -61]

所以servlet端,当使用request.getParameter的时候内部应该是调用
String s = new String(bytes, response.getEncoding())的,如果你response没有设置编码,那么就采用默认的编码null会转为java 平台的GBK,那中文就变成乱码了。

所以为了避免乱码,jsp站点一般设一个过滤器,所有的页面、servet都设置统一的编码。response.setEncoding, request.setEncoding.

Java的String内部是一个char[], char是一个用16位存储的utf-16编码的单元。为此,当要把字符、字符串转为字节输出到文件、网络,或者从文件、网络读到的字节流还原为有实际意义的字符,都要明白其编码是什么。

转码

知道基础知识转码就比较简单了:

普通编码转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package vip.infotech.charset;

import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.util.Arrays;

/**
* @ClassName:
* @Description: 编码转换
* @author: ljp
* @Copyright: www.infotech.vip, Ltd. All rights reserved.
*/
public class CodeChange {
public static void main(String[] args) throws UnsupportedEncodingException {
System.out.println("当前JRE:" + System.getProperty("java.version"));
System.out.println("当前JVM的默认字符集:" + Charset.defaultCharset());
String str = "李四";
// to gbk
byte[] gbkByteArray = str.getBytes("GBK");
String gbkCode = new String(gbkByteArray, "GBK");
System.out.println("gbkByteArray:" + Arrays.toString(gbkByteArray));
System.out.println("GBKString:" + gbkCode);

// to utf-8
byte[] utfByteArray = str.getBytes("UTF-8");
String utfCode = new String(utfByteArray, "UTF-8");
System.out.println("utf-8ByteArray:" + Arrays.toString(utfByteArray));
System.out.println("utf-8String:" + utfCode);

}
}

网页编码转换

网页的编码转换可以通过iso-8859-1来进行中转,保证网页传给Tomcat的编码格式是iso8859-1。java获取iso8859-1字符集字节码,通过new String(,"UTF-8")用UTF-8解码

js网页:

1
encodeURI('中文字符')

java后台处理

1
new String(str.getBytes("ISO8859-1"),"UTF-8")

实例-测试DEMO

实例一

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
package vip.infotech.charset;

import java.io.*;
import java.nio.charset.Charset;
import java.util.Arrays;

/**
* @ClassName:
* @Description: 字符集信息测试
* @author: ljp
* @Copyright: www.infotech.vip. All rights reserved.
*/
public class CharsetTest {
public static void main(String[] args) throws IOException {
File directory = new File("");//获取工程当前目录
String filaPath = directory.getAbsolutePath()+"/learn-basic/src/main/java/vip/infotech/charset/encode.txt";
System.out.println(directory.getAbsolutePath());

System.out.println("当前JRE:" + System.getProperty("java.version"));
System.out.println("当前JVM的默认字符集:" + Charset.defaultCharset());

String str = "中文";
// 获取JVM默认字符集
System.out.println("defaultCharset:" + Charset.defaultCharset());

System.out.println("##字符串转换成byte数组");
byte[] defaultByteArray = str.getBytes();
byte[] gbkByteArray = str.getBytes("GBK");
byte[] utfByteArray = str.getBytes("UTF-8");
System.out.println("defaultByteArray:"
+ Arrays.toString(defaultByteArray));
System.out.println("gbkByteArray:" + Arrays.toString(gbkByteArray));
System.out.println("utfByteArray:" + Arrays.toString(utfByteArray));

System.out.println("##byte数组转换成字符串");
String defaultStr = new String(defaultByteArray);
String gbkStr = new String(defaultByteArray, "GBK");
String utfStr = new String(defaultByteArray, "UTF-8");
System.out.println("defaultStr:" + defaultStr);
System.out.println("gbkStr:" + gbkStr);
System.out.println("utfStr:" + utfStr);

System.out.println("##字节流转化成字符流");
System.out.println(System.getProperty("user.dir"));
// 文件中只有“中文”2个字,文件采用“UTF-8”编码,共6个byte
BufferedReader defaultReader = new BufferedReader(
new InputStreamReader(new FileInputStream(filaPath)));
BufferedReader gbkReader = new BufferedReader(new InputStreamReader(
new FileInputStream(filaPath), "GBK"));
BufferedReader utfReader = new BufferedReader(new InputStreamReader(
new FileInputStream(filaPath), "UTF-8"));
System.out.println("defaultReader:" + defaultReader.readLine());
System.out.println("gbkReader:" + gbkReader.readLine());
System.out.println("utfReader:" + utfReader.readLine());

System.out.println("##字符流转化成字节流");
BufferedWriter defaultWriter = new BufferedWriter(
new OutputStreamWriter(System.out));
BufferedWriter gbkWriter = new BufferedWriter(new OutputStreamWriter(
System.out, "GBK"));
BufferedWriter utfWriter = new BufferedWriter(new OutputStreamWriter(
System.out, "UTF-8"));
System.out.print("defaultWriter:");
defaultWriter.write(str);
// 这里不能用close()方法,否则System.out也被关闭,后续无输出
defaultWriter.flush();
System.out.print("\r\ngbkReader:");
gbkWriter.write(str);
gbkWriter.flush();
System.out.print("\r\nutfReader:");
utfWriter.write(str);
utfWriter.flush();

}
}

执行结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
D:\Source\learn\java-learning-code
当前JRE:1.8.0_171
当前JVM的默认字符集:UTF-8
defaultCharset:UTF-8
##字符串转换成byte数组
defaultByteArray:[-28, -72, -83, -26, -106, -121]
gbkByteArray:[-42, -48, -50, -60]
utfByteArray:[-28, -72, -83, -26, -106, -121]
##byte数组转换成字符串
defaultStr:中文
gbkStr:涓枃
utfStr:中文
##字节流转化成字符流
D:\Source\learn\java-learning-code
defaultReader:中国
gbkReader:涓浗
utfReader:中国
##字符流转化成字节流
defaultWriter:中文
gbkReader:����
utfReader:中文

实例二

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package vip.infotech.charset;

import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
public class Code {
public static void main(String[] args) throws UnsupportedEncodingException {
System.out.println("当前JRE:" + System.getProperty("java.version"));
System.out.println("当前JVM的默认字符集:" + Charset.defaultCharset());
System.out.println(System.getProperty("file.encoding")); //GBK
System.out.println(System.getProperty("user.language")); //zh
System.out.println(System.getProperty("user.region")); //CN

String str = "张三" ;
byte[] codeByte=str.getBytes();//编码
//eclipse文件默认gbk--》gb2312,如果把文件修改成utf-8格式输出的字节码跟getBytes("UTF-8"一样)
//如果用默认的GBK模式default获取的字节码跟gb2312一样
System.out.println("default===="+bytesToHex(codeByte));
byte[] codeByteUTF=str.getBytes("UTF-8");//编码
System.out.println("UTF-8===="+bytesToHex(codeByteUTF));
byte[] decodeGB= str.getBytes("gb2312") ; //编码
System.out.println("gb2312===="+bytesToHex(decodeGB));
String codeGB = new String(decodeGB,"gb2312");//解码 如果上面的解码不对 可能出现问题
String codeUTF = new String(codeByteUTF,"UTF-8");//解码 如果上面的解码不对 可能出现问题
System.out.println("gb2312===="+codeGB);
System.out.println("UTF-8===="+codeUTF);
}


public static String bytesToHex(byte[] src) {
if (src == null || src.length <= 0) {
return null;
}
StringBuilder stringBuilder = new StringBuilder("");
for (int i = 0; i < src.length; i++) {
// 之所以用byte和0xff相与,是因为int是32位,与0xff相与后就舍弃前面的24位,只保留后8位
String str = Integer.toHexString(src[i] & 0xff);
if (str.length() < 2) { // 不足两位要补0
stringBuilder.append(0);
}
stringBuilder.append(str);
}
return stringBuilder.toString();
}
}

结果

1
2
3
4
5
6
7
8
9
10
当前JRE:1.8.0_171
当前JVM的默认字符集:UTF-8
UTF-8
zh
null
default====e5bca0e4b889
UTF-8====e5bca0e4b889
gb2312====d5c5c8fd
gb2312====张三
UTF-8====张三

实例三

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
package test;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.util.Arrays;
//java文件为gbk于utf-8的结果,证明运行的时候JVM的默认字符集会根据java保存的文件格式变
public class Code2 {
public static void main(String[] args) throws IOException {
System.out.println("当前JRE:" + System.getProperty("java.version"));
System.out.println("当前JVM的默认字符集:" + Charset.defaultCharset());
System.out.println(System.getProperty("file.encoding")); //GBK
System.out.println(System.getProperty("user.language")); //zh
System.out.println(System.getProperty("user.region")); //CN
InputStreamReader isr = new InputStreamReader(System.in, "iso8859-1");//先按gbk编码再iso8859-1解码
// Create an InputStreamReader that uses the given charset decoder
BufferedReader br = new BufferedReader(isr);
String strLine = br.readLine();
br.close();
isr.close();
System.out.println(strLine);
System.out.println(new String(strLine.getBytes(), "iso8859-1"));// 错误改法,gbk编码-iso解码
// Encodes this String (strLine) into a sequence of bytes using the
// platform's
// default charset(gb2312) then constructs a new String by decoding the
// specified array of bytes using the specified charset (iso8859-1)
// because this String (strLine) uses the charset decoder "iso8859-1",so
// it can
// only be encoded by "iso8859-1",cann't be encoded by the platform's
// default
// charset "gb2312",so this line is wrong.
System.out.println(new String(strLine.getBytes("iso8859-1")));// 正确改法,iso编码,gbk解码
// Encodes this String (strLine) into a sequence of bytes using the
// named
// charset (iso8859-1),then constructs a new String by decoding the
// specified array of bytes using the platform's default charset
// (gb2312).
// This line is right.
System.out.println(bytesToHex(strLine.getBytes("iso8859-1")));
System.out.println(new String("我我我我".getBytes("iso8859-1"),"iso8859-1"));
System.out.println(bytesToHex("我我我我".getBytes("iso8859-1")));//我我我我默认是文件编码所以:gbk解码成字符串用iso编码成字节流
String iso = new String("我我我我".getBytes("UTF-8"),"ISO-8859-1");
System.out.println(iso);
System.out.println(Arrays.toString(iso.getBytes("ISO-8859-1")));
System.out.println(new String(iso.getBytes("ISO-8859-1"),"UTF-8"));

iso = new String("我我我我".getBytes("GBK"),"ISO-8859-1");
System.out.println(iso);
System.out.println(Arrays.toString(iso.getBytes("ISO-8859-1")));
System.out.println(new String(iso.getBytes("ISO-8859-1"),"GBK"));
}
public static String bytesToHex(byte[] src) {
if (src == null || src.length <= 0) {
return null;
}
StringBuilder stringBuilder = new StringBuilder("");
for (int i = 0; i < src.length; i++) {
// 之所以用byte和0xff相与,是因为int是32位,与0xff相与后就舍弃前面的24位,只保留后8位
String str = Integer.toHexString(src[i] & 0xff);
if (str.length() < 2) { // 不足两位要补0
stringBuilder.append(0);
}
stringBuilder.append(str);
}
return stringBuilder.toString();
}
}

运行结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
                文件为GBK格式的结果
* 当前JRE:1.8.0_91
当前JVM的默认字符集:GBK
GBK
zh
null
我我我我
????????
????????
我我我我
ced2ced2ced2ced2
????
3f3f3f3f
????????????
[-26, -120, -111, -26, -120, -111, -26, -120, -111, -26, -120, -111]
我我我我
????????
[-50, -46, -50, -46, -50, -46, -50, -46]
我我我我

文件为UTF-8的结果
当前JRE:1.8.0_91
当前JVM的默认字符集:UTF-8
UTF-8
zh
null
我我我我
我我我我
我我我我
我我我我
e68891e68891e68891e68891
????
3f3f3f3f
我我我我
[-26, -120, -111, -26, -120, -111, -26, -120, -111, -26, -120, -111]
我我我我
ÎÒÎÒÎÒÎÒ
[-50, -46, -50, -46, -50, -46, -50, -46]
我我我我