您的位置:首页 > 编程语言 > Java开发

【基础知识】字符集编码、Java中编码的格式以及乱码产生的原因

2017-01-05 19:08 525 查看

1 字符集的编码总结

1.1 ASCII码的引入

计算机最早在美国使用,八位的字节一共可以组合出256(2的8次方)种不同的状态。最高位为0,剩余的7位用来表示ASCII码值。把第0-31位及第127位【共33位】,用来表示控制字符或通讯专用字符(其余为可显示字符)。把32-126【共95个】用来表示空格、标点符号、数字、大小写字母等字符。每一个字符都用一个数字来表示,一个字节所能表示的数字范围内足以容纳所有的这些字符。这样计算机就可以用不同字节来存储英语的文字了。这种字符与数字的编码固定下来以后,这套方案称为ASCII编码【American Standard Code for Information Interchange,美国信息互换标准代码】

1.2 ISO-8859-1编码。

ASCII编码采用的是7位字符编码方案,最高位为0。ASCII编码,只包含了英文的所有字符,对于西欧国家,把ASCII的最高位利用起来,制定出ISO-8859-1【又称Latin-1】,是一个8位字节字符集。新增理论空间128,但是只用了其中的96个,主要是一些西欧的字符。他兼容ASCII编码字符集。但是ISO-8859-1却无法表示中文字符集,怎么办。

1.3 其他码制的引入。【GB2312, GBK, Big-5】

随着计算机在其他国家的应用和普及,许多国家都把本地的字符集引入了计算机。一个字节所表示的范围是不能容纳所有的中文汉字。中国大陆将每一个简体中文都用2个字节表示,原有的ASCII的编码保持不变。仍然采用一个字节表示。为了将1个中文字符与2个ASCII码字符相区别,中文字符的每个字节的最高位(bit)都为1。这套编码规则称为GB2312编码。GB2312包含的汉字有限,后来又在GB2312的基础上进行了扩展,并且包括繁体进行编码,新的编码系统就是GBK(国标码)。

GBK向下兼容GB2312。中国台湾使用中文字符编码规则称为big-5(大五码),一个汉字用2个字节表示。

1.4 unicode编码。

不同地区和国家使用的本地化字符编码标准,严重制约了各地区的计算机使用和技术方面交流。国际标准化组织(ISO)制定unicode编码:规定所有的字节使用2个字节表示,即认定全世界所有的字符个数不会超过2的16次方(65535)【实际上肯定超过】。对于ASCII里那些字符,unicode保持原编码不变,只是将其长度由原来的8位扩展为16位。而将其他的语言的字符全部重新统一编码。【但是这种编码方式会使保存英文文本时,多浪费1倍存储空间】

1.5 UTF-8编码。

UTF-8编码是为了解决unicode编码的缺点而诞生的。

unicode编码是定长的双字节,对于英文字母,unicode也需要2个字节表示。对于文件的存储和传输都是极大的资源浪费。

UTF-8是在互联网上使用最广的一种Unicode的实现方式。它是一种变长的编码方式,可以使用1-4个字节表示一个符号。

UTF-8编码规则:

①对于单字节符号:字节的第一位设为0,后面7位为这个符号的unicode码。英文字符,UTF-8编码和ASCII码是相同的。

②如果是多字节,其第一个字节从最高位开始,连续的二进制位值为1的个数决定了其编码的位数,其余各字节均以10开头。

例如:

Unicode符号范围 | UTF-8编码方式

(十六进制) | (二进制)

——————–+———————————————

0000 0000-0000 007F | 0xxxxxxx

0000 0080-0000 07FF | 110xxxxx 10xxxxxx

0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx

0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

通常情况下UTF-8编码下,一个汉字通常占用3个字节。

2 Java中字符编码和乱码产生的原因

2.1 Java中的字符到底以何种编码方式存放在内存中?

在Java中的字符使用的都是unicode编码,Java技术通过Unicode保证跨平台特性【即Java字符在内存中总是以unicode编码存储的】。同时也支持了全扩展的本地平台字符,我们显示输出和键盘输入都是采用的本地编码。

Java采用的unicode字符编码,每个字符占用2个字节,我们直接把每个字符中的内容对应着的整数打印出来,显示的结果就是这个字符的unicode码。

String类中的getBytes()方法:不是直接将字符串中的每个字节数据存放到一个字节数组。而是将unicode码的字符串中的每个字符数字,转换成该字符在指定的字符集下的数字,最后将这些数字存放到一个数组中返回。

JDK中的编码、解码:将一个字符的unicode码转换成某种本地字符集码的过程叫做编码,将unicode码装换成到本地字符集,在JDK包中必须有对应的字符集编码器。

public class CharCode {
public static void main(String[] args) throws Exception{
//1.直接打印Java中字符对应,编码十六进制
String strChina = "中国";
for(int i=0; i<strChina.length(); i++){
System.out.println(Integer.toHexString((int)strChina.charAt(i)));
}
/* Java字符的Unicode编码: 4e2d 56fd*/
//2.获取指定字符编码集下的十六进制值
byte [] buf_1=strChina.getBytes("gb2312");
for(int i=0; i<buf_1.length; i++){
System.out.println(Integer.toHexString(buf_1[i]));
}
/*GB2312编码集: ffffffd6 ffffffd0 ffffffb9 fffffffa*/
//3.使用本地默认字符编码集
System.out.println("当前JVM的默认字符集:" + Charset.defaultCharset());
byte [] buf_2=strChina.getBytes();
for(int i=0; i<buf_2.length; i++){
System.out.println(Integer.toHexString(buf_2[i]));
}
/*本地默认编码(GBK):     ffffffd6 ffffffd0 ffffffb9 fffffffa*/
}
}


2.2 Java是如何来进行编码、解码的

第一步:我们使用编辑器编写的Java源文件,在保存时会采用操作系统默认的编码格式来形成一个.java文件。【一般中文的操作系统采用的是GBK编码格式】

java中file.encoding保存的是系统默认编码格式,采用如下语句可打印查看:

System.out.println(System.getProperty(“file.encoding”));

第二步:当我们使用javac.exe编译我们的java文件时,JDK首先会确认它的编译参数encoding来确定源代码字符集,如果我们不指定该编译参数,JDK首先会获取操作系统默认的file.encoding参数,然后JDK就会把我们编写的java源程序从file.encoding编码格式转化为Java内部默认的unicode格式存入内存中。

**第三步:**Jdk将上面编译好的且保存在内存中信息写入class文件中,形成.class文件。此时.class文件是Unicode编码的,也就是说我们常见的.class文件中的内容无论是中文字符还是英文字符,他们都已经转换为Unicode编码格式了。

第四步:运行编译的类,这里需要区分几种情况。

**(1)Console上运行的类:**Jvm首先会把保存在操作系统中的class文件读入到内存中,这个时候内存中class文件编码格式为Unicode,然后Jvm运行它。如果需要用户输入信息,则会采用file.encoding编码格式对用户输入的信息进行编码同时转换为Unicode编码格式保存到内存中。程序运行后,将产生的结果再转化为file.encoding格式返回给操作系统并输出到界面去.

(2)JSP/Servlet类:当用户请求Servlet时,Web容器会调用它的Jvm来运行Servlet。首先Jvm会把Servlet的class加载到内存中去,内存中的Servlet代码是Unicode编码格式的。然后Jvm在内存中运行该Servlet,在运行过程中如果需要接受从客户端传递过来的数据(如表单和URL传递的数据),则Web容器会接受传入的数据,在接收过程中如果程序设定了传入参数的的编码则采用设定的编码格式,如果没有设置则采用默认的ISO-8859-1编码格式,接收的数据后Jvm会将这些数据进行编码格式转换为Unicode并且存入到内存中。运行Servlet后产生输出结果,同时这些输出结果的编码格式仍然为Unicode。紧接着WEB容器会将产生的Unicode编码格式的字符串直接发送置客户端,如果程序指定了输出时的编码格式,则按照指定的编码格式输出到浏览器,否则采用默认的ISO-8859-1编码格式。

(3)Java类与数据库之间:我们知道java程序与数据库的连接都是通过JDBC驱动程序来连接的,而JDBC驱动程序默认的是ISO-8859-1编码格式的,也就是说我们通过java程序向数据库传递数据时,JDBC首先会将Unicode编码格式的数据转换为ISO-8859-1的编码格式,然后在存储在数据库中,即在数据库保存数据时,默认格式为ISO-8859-1。

2.3 Java编码解码的场景

在java中主要有四个场景需要进行编码解码操作:I/O操作、内存、数据库、javaWeb

(1)I/O操作:java读取文件的方式分为按字节流读取和按字符流读取,其中InputStream、Reader是这两种读取方式的超类。

按字节读取数据:

我们一般都是使用InputStream.read()方法在数据流中读取字节(read()每次都只读取一个字节,效率非常慢,我们一般都是使用read(byte[])),然后保存在一个byte[]数组中,最后转换为String。在我们读取文件时,读取字节的编码取决于文件所使用的编码格式,而在转换为String过程中也会涉及到编码的问题,如果两者之间的编码格式不同可能会出现问题。

例如:存在一个问题test.txt编码格式为UTF-8,那么通过字节流读取文件时所获得的数据流编码格式就是UTF-8,而我们在转化成String过程中如果不指定编码格式,则默认使用系统编码格式(GBK)来解码操作,由于两者编码格式不一致,那么在构造String过程肯定会产生乱码。

解决乱码的方式:在构造String过程中指定编码格式,使得编码解码时两者的字符集保持一致。

public class CodeTest {
public static void main(String[] args) throws IOException {
File utf_8file = new File("test.txt"); //文本内容保存为UTF-8格式
InputStream input = new FileInputStream(utf_8file);
StringBuffer buffer = new StringBuffer();
byte[] bytes = new byte[1024];
for(int n ; (n = input.read(bytes))!=-1 ; ){
//buffer.append(new String(bytes,0,n)); 系统默认使用GBK编码,会乱码
buffer.append(new String(bytes,0,n,"UTF-8"));
}
System.out.println(buffer);
}
}


(2)内存:

(3)数据库:

(4)javaWeb:

3 String类的getBytes( )方法与构造函数

3.1 getBytes(charset):从字符串获取指定二进制字节数组

字符串处理函数。将字符串所表示的字符按照charset编码,并以字节表示。将字符串所表示的字符按照charset编码,并以字节表示。即getBytes()将unicode码转换成制定的charset编码。

String(byte[] bytes,String charsetName):String类的构造函数。

通过使用指定解码方式,将unicode编码的字节数组解码成指定格式的字符串,构造一个新的 String。

列出系统中的默认编码集:System.getProperties().list(System.out);

修改系统的默认编码集:System.getProperties().put(“file.encoding”,”GBK”);

在JVM中、在内存中、在代码里声明的每一个char、String类型的变量中字符以unicode格式存在。

①String str = “中”;

②byte[ ] bytes = str.getBytes( );

③bytes = str.getBytes(“ISO-8859-1”);

语句①:将一个只含有一个字符”中”的字符串文字量赋给String类的一个对象str。

字符文字量”中”是按照操作系统默认编码方式进行编码,在中文 windows 系统中通常是”GBK”,”中”在GBK编码中是0xD6D0,

在将该字符赋给str时,Java会对该字符串进行编码转换,即将GBK编码方式的”中”转换成Unicode编码方式的”中”,【此过程在编译时进行的】Unicode编码方式”中”的编码是0x4E2D,所以str在程序运行期间在内存中的二进制表示成16进制就是0x4E2D。

语句②:获得str字符串的二进制形式。

getBytes(String encoding)方法需要指定编码方式,表示获得该字符串在何种编码方式中的二进制形式。

此语句中没有设置参数,表示采用操作系统默认的编码方式,即此处获得的bytes是”中”在GBK编码中的二进制形式,即bytes[0]=0xD6, bytes[1]=0xD0。【将内存中的Unicode编码字节数组转换成GBK形式的编码格式】

语句③:该语句与语句②的区别就是指定了编码方式,此处指定的是ISO-8859-1,即通常所说的Latin-1,该编码采用8bit对字符编码,所以编码空间中只有256个字符。该编码中只包含了基本的ASCII码和一些扩展的其它西欧字符,所以该字符集中不可能包含中文的”中”字,也就是说Java虚拟机无法在ISO-8859-1编码集中找到”中”字对应的编码,

针对这种情况,就只返回一个问号(?,0x3f)字符,所以此时bytes.length只有1,且bytes[0]=0x3f。

3.2 String(byte[] bytes, String encoding):从二进制字节数组构造字符串

getBytes()方法从字符串获得二进制的字节数组。如果要从二进制的字节数组获得字符串,则就需要使用new String(byte[] bytes, String encoding)方法,该方法按照encoding编码方法对字节数组bytes中的二进制数组进行解析,生成一个新的字符串对象。

①byte[] bytes = {(byte)0xD6, (byte)0xD0, (byte)0x31};

②String str = new String(bytes);

③str = new String(bytes,”ISO-8859-1”);

语句①:定义一个字节数组。

语句②:将该字节数组中的二进制数据按照默认的编码方式(GBK)编码成字符串,我们知道GBK中0xD6 0xD0表示”中”,0x31表示字符”1”(GBK兼容ASCII,但不兼容ISO-8859-1除ASCII之外的部分),所以str得到的值是”中1”。

语句③:该句用ISO-8859-1编码方式对该字节数据进行编码,由于在ISO-8859-1编码方式中一个字节会被解析成一个字符,所以该字节数组会被解释成包含三个字符的字符串,但由于在ISO-8859-1编码方式中没有对应0xD6和0xD0的字符,所以前两个字符会产生两个问号,由于0x31在ISO-8859-1编码中对应字符”1”(ISO-8859-1也兼容ASCII),所以此语句得到str是”??1”。

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

Java文件的编码可能有多种多样,但Java编译器自动将这些编码按照Java文件的编码格式正确读取后产生class文件,

不论在编译前Java文件使用何种编码,在编译后生成的class文件,他们都是Unicode。

Java正确的做字符串编码转换:

字符串在Java中统一使用unicode表示。

源文件:String str = “中国”;

如果源文件是GBK编码, 操作系统(Windows)默认的环境编码为GBK,

那么编译时,JVM将按照GBK编码将字节数组解析成字符,然后将字符转换为unicode格式的字节数组,作为内部存储。

当打印这个字符串是,JVM根据操作系统本地的语言环境,将unicode转换为GBK,然后操作系统将GBK格式的内容显示出来。

当源码文件是UTF-8, 我们需要通知编译器源码的格式,javac -encoding utf-8 … , 编译时,

JVM按照utf-8 解析成字符,然后转换为unicode格式的字节数组, 那么不论源码文件是什么格式,同样的字符串,

最后得到的unicode字节数组是完全一致的,显示的时候,也是转成GBK来显示(跟OS环境有关)

3.4 为什么会产生乱码??

根本原因:字符创原本的编码格式与读取时解析用的编码格式不一致导致的。

String str = “中国”;

System.out.println( new String(s.getBytes(),”UTF-8”));

//错误,因为getBytes()默认使用GBK编码,而解析时使用UTF-8编码,肯定出错。

其中 getBytes() 是将unicode 转换为操作系统默认的格式的字节数组,即”中国”的 GBK格式,

new String (bytes, Charset) 中的charset 是指定读取 bytes 的方式,这里指定为UTF-8,

即把bytes的内容当做UTF-8 格式对待。

正确的编解码方式,源内容编码和解析用的编码是一致的。

System.out.println( new String(s.getBytes(),”GBK”));

System.out.println( new String(s.getBytes(“UTF-8”),”UTF-8”));

3.5 如何利用getBytes 和 new String() 来进行编码转换

网上流传着一种错误的方法:

GBK–> UTF-8:

new String(s.getBytes(“GBK”),”UTF-8); 这种方式是完全错误的,因为getBytes 的编码与UTF-8 不一致,肯定是乱码。

但是为什么在Tomcat 下,使用 new String(s.getBytes(“iso-8859-1”) ,”GBK”) 却可以用呢? 原因如下:

Tomcat 默认使用iso-8859-1编码,即如果原本字符串是GBK的,Tomcat传输过程中,将GBK转成了iso-8859-1。

例如汉字”中”在GBK编码后得到2字节的值,然后通过GET或POST把这2个字节提交到Tomcat容器,如果你不告诉Tomcat我的参数是用GBK编码的。Tomcat默认以为传来的是以ISO-8859-1来编码的。然后Tomcat就使用ISO-8859-1来解码,解码后,字符串”中”在JVM中是以Unicode的形式存在的。ISO8859-1是兼容ASCII的单字节编码并且使用了单字节内的所有空间,在支持ISO-8859-1的系统中传输和存储其他任何编码的字节流都不会被抛弃。换言之,把其他任何编码的字节流当作ISO-8859-1编码看待都没有问题。所以GBK编码的”中”被Tomcat当成了2个字节的iso-8859-1来进行解码,解码后的字符串以Unicode的形式存在JVM中,在终端需要,将unicode字节,编码成指定的字符。s.getBytes(“iso-8859-1”)将unicode字节数组转换成ISO-8859-1编码的字节,再用String()构造成制定的字符格式。【不乱码,仅仅是一种巧合】

如何正确的将GBK转UTF-8 ? (实际上是unicode转UTF-8)

1、源码文件是GBK格式,或者这个字符串是从GBK文件中读取出来的, 转换为string 变成unicode格式

String gbkStr = “中国”;

2、利用getBytes将unicode字符串转成UTF-8格式的字节数组

byte[] utf8Bytes = gbkStr.getBytes(“UTF-8”);

3、然后用utf-8 对这个字节数组解码成新的字符串

String utf8Str = new String(utf8Bytes, “UTF-8”);

简化以上3个步骤:

unicodeToUtf8 (String s) {

return new String( s.getBytes(“utf-8”) , “utf-8”);

}

UTF-8 转GBK原理同理:

return new String( s.getBytes(“GBK”) , “GBK”);

核心工作都由 getBytes(charset)处理。

getBytes 的JDK 描述:Encodes this String into a sequence of bytes using the named charset, storing the result into a new byte array.
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  编码 utf-8 unicode
相关文章推荐