Java 编码与字符

一、字符集介绍

ANSIAmerican National Standards Institute。中文:美国国家标准学会

不同国家的和地区为此制定了不同标准,由此产生了 GB2312、GBK、Big5、Shift_JIS 等各自的编码标准。这些使用 1 至 4 个字节来代表一个字符的各种汉字延伸编码方式,称为 ANSI 编码。在简体中文Windows操作系统中,ANSI 编码代表 GBK 编码;在日文Windows操作系统中,ANSI 编码代表 Shift_JIS 编码。 不同 ANSI 编码之间互不兼容,当信息在国际间交流时,无法将属于两种语言的文字,存储在同一段 ANSI 编码的文本中。

UNICODEUnicode是国际组织制定的可以容纳世界上所有文字和符号的字符编码方案。Unicode用数字0-0x10FFFF来映射这些字符,最多可以容纳1114112个字符,或者说有1114112个码位。码位就是可以分配给字符的数字。UTF-8UTF-16UTF-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
UnicodeUTF-8的转换:Unicode16进制编码<-->对应的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倍。

小结:

ASCIIGB2312GBKGB18030Big5Unicode都是字符集的名称。它们定义了采用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通信,在传输的时候也要采用相同的字符集

你可能感兴趣的:(java)