从Java String实例来理解ANSI、Unicode、BMP、UTF等编码概念

澳门新葡亰3522平台游戏 7

一、前言

一切的谜都解开了!在写这篇随笔之前,我的心情只能用金田一每次破案后的这句台词来表达。

其实从开始写Java代码以来,遇到过无数次乱码与转码问题,比如从文本文件读入到String出现乱码,JSP获取HTTP请求参数出现乱码等问题,由于这些问题很常见,遇到的时候随手百度一下一般都可以顺利解决。也曾尝试过去把概念理清楚,但网上众说纷纭,内容繁杂,又不愿意花精力去看标准文档,所以问题搁置了很久。

前两天同学与我谈起一个Java源文件的编码问题(这问题在最后一个实例分析),从这个问题入手拉扯出了一连串的问题,然后我们一边查资料一边讨论,直到深夜,终于在一篇博客中找到了关键性线索,解决了所有的疑惑,以前没有理解的语句都能解释清楚了。因此我决定用这篇随笔,记录我对一些编码问题的理解以及实验的结果。

下面有些概念是我自己结合实际的理解,如果有误,请一定不吝指正。

转()

二、概念总结

早期,互联网还没有发展起来,计算机仅用于处理一些本地的资料,所以很多国家和地区针对本土的语言设计了编码方案,这种与区域相关的编码统称为ANSI编码(因为都是对ANSI-ASCII码的扩展)。但是他们没有事先商量好怎么相互兼容,而是自己搞自己的,这样就埋下了编码冲突的祸根,比如大陆使用的GB2312编码与台湾使用的Big5编码就有冲突,同样的两个字节,在两种编码方案里表示的是不同的字符,随着互联网的兴起,一个文档里经常会包含多种语言,计算机在显示的时候就遇到麻烦了,因为它不知道这两个字节到底属于哪种编码。

这样的问题在世界上普遍存在,因此重新定义一个通用的字符集,为世界上所有字符进行统一编号的呼声不断高涨。

由此Unicode码应运而生,它为世界上所有字符进行了统一编号,由于它可以唯一标识一个字符,所以字体也只需要针对Unicode码进行设计就行了。但Unicode标准定义的是一个字符集,而没有规定编码方案,也就是说它仅仅定义了一个个抽象的数字与其对应的字符,而没有规定具体怎么存储一串Unicode数字,真正规定怎么存储的是UTF-8、UTF-16、UTF-32等方案,所以带有UTF开头的编码,都是可以直接通过计算和Unicode数值(Code
Point,代码点)进行转换的。
顾名思义,UTF-8就是8位长度为基本单位编码,它是变长编码,用1~6个字节来编码一个字符(因为受Unicode范围的约束,所以实际最大只有4字节);UTF-16是16位为基本单位编码,也是变长编码,要么2个字节要么4个字节;UTF-32则是定长的,固定4字节存储一个Unicode数。

其实我以前一直对Unicode有点误解,在我的印象中Unicode码最大只能到0xFFFF,也就是最多只能表示
2^16
个字符,在仔细看了维基百科之后才明白,早期的UCS-2编码方案确实是这样,UCS-2固定使用两个字节来编码一个字符,因此它只能编码BMP(基本多语言平面,即0×0000-0xFFFF,包含了世界上最常用的字符)范围内的字符。为了要编码Unicode大于0xFFFF的字符,人们对UCS-2编码进行了拓展,创造了UTF-16编码,它是变长的,在BMP范围内,UTF-16与UCS-2完全一致,而BMP之外UTF-16则使用4个字节来存储。

为了方便下面的描述,先交代一下代码单元(Code
Unit)的概念,某种编码的基本组成单位就叫代码单元,比如UTF-8的代码单元为1个字节,UTF-16的代码单元为2个字节,不好解释,但是很好理解。

为了兼容各种语言以及更好的跨平台,Java
String保存的就是字符的Unicode码。它以前使用的是UCS-2编码方案来存储Unicode,后来发现BMP范围内的字符不够用了,但是出于内存消耗和兼容性的考虑,并没有升到UCS-4(即UTF-32,固定4字节编码),而是采用了上面所说的UTF-16,char类型可看作其代码单元。这个做法导致了一些麻烦,如果所有字符都在BMP范围内还没事,若有BMP外的字符,就不再是一个代码单元对应一个字符了,length方法返回的是代码单元的个数,而不是字符的个数,charAt方法返回的自然也是一个代码单元而不是一个字符,遍历起来也变得麻烦,虽然提供了一些新的操作方法,总归还是不方便,而且还不能随机访问。

此外,我发现Java在编译的时候还不会处理大于0xFFFF的Unicode字面量,所以如果你敲不出某个非BMP字符来,但是你知道它的Unicode码,得用一个比较笨的方法来让String存储它:手动计算出该字符的UTF-16编码(四字节),把前两个字节和后两个字节各作为一个Unicode数,然后赋值给String,示例代码如下所示。

public static void main(String[] args) {
        //String str = "";        //我们想赋值这样一个字符,假设我输入法打不出来

        //但我知道它的Unicode是0x1D11E
        //String str = "u1D11E";  //这样写不会识别

        //于是通过计算得到其UTF-16编码 D834 DD1E
        String str = "uD834uDD1E"; //然后这么写

        System.out.println(str);     //成功输出了""
    }

Windows系统自带的记事本可以另存为Unicode编码,实际上指的是UTF-16编码。上面说了,主要使用的字符编码都在BMP范围内,而在BMP范围内,每个字符的UTF-16编码值与对应的Unicode数值是相等的,这大概就是微软把它称为Unicode的原因吧。举个例子,我在记事本中输入了”好a“两个字符,然后另存为Unicode
big
endian(高位优先)编码,用WinHex打开文件,内容如下图,文件开头两个字节被称为Byte
Order Mark(字节顺序标记),(FE FF)标识字节序为高位优先,然后(59
7D)正是”好“的Unicode码,(00 61)正是”a“的Unicode码。

澳门新葡亰3522平台游戏 1

有了Unicode码,也还不能立即解决问题,因为首先世界上已经存在了大量的非Unicode标准的编码数据,我们不可能丢弃它们,其次Unicode的编码往往比ANSI编码更占空间,所以从节约资源的角度来说,ANSI编码还是有存在的必要的。所以需要建立一个转换机制,使得ANSI编码可以转换到Unicode进行统一处理,也可以把Unicode转换到ANSI编码以适应平台的要求。

转换方法说起来比较容易,对于UTF系列或者是ISO-8859-1这种被兼容的编码,可以通过计算和Unicode数值直接进行转换(实际可能也是查表),而对于系统遗留下来的ANSI编码,则只能通过查表的方式进行,微软把这种映射表称为Code
Page(代码页),并按编码进行分类编号,比如我们常见的cp936就是GBK的代码页,cp65001就是UTF-8的代码页。下图是微软官网查到的GBK->Unicode映射表(目测不全),同理还应有反向的Unicode->GBK映射表。

澳门新葡亰3522平台游戏 2

有了代码页,就可以很方便的进行各种编码转换了,比如从GBK转换到UTF-8,只需要先按照GBK的编码规则对数据按字符划分,用每个字符的编码数据去查GBK代码页,得到其Unicode数值,再用该Unicode去查UTF-8的代码页(或直接计算),就可以得到对应的UTF-8编码。反过来同理。注意:UTF-8是Unicode的标准实现,它的代码页中包含了所有的Unicode取值,所以任意编码转换到UTF-8,再转换回去都不会有任何丢失。至此,我们可以得出一个结论就是,要完成编码转换工作,最重要的是第一步要成功的转换到Unicode,所以正确选择字符集(代码页)是关键。

理解了转码丢失问题的本质后,我才突然明白JSP的框架为什么要以ISO-8859-1去解码HTTP请求参数,导致我们获取中文参数的时候不得不写这样的语句:

String param = new String(s.getBytes("iso-8859-1"), "UTF-8");

因为JSP框架接收到的是参数编码的二进制字节流,它不知道这究竟是什么编码(或者不关心),也就不知道该查哪个代码页去转换到Unicode。然后它就选择了一种绝对不会产生丢失的方案,它假设这是ISO-8859-1编码的数据,然后查ISO-8859-1的代码页,得到Unicode序列,因为ISO-8859-1是按字节编码的,而且不同于ASCII的是,它对0
~
255空间的每一位都进行了编码,所以任意一个字节都能在它的代码页中找到对应的Unicode,若再从Unicode转回原始字节流的话也就不会有任何丢失。它这样做,对于不考虑其他语言的欧美程序员来说,可以直接用JSP框架解码好的String,而要兼容其他语言的话也只需要转回原始字节流,再以实际的代码页去解码一下就好。

我对Unicode以及字符编码的相关概念阐述完毕,接下来用Java实例来感受一下。

概念总结

早期,互联网还没有发展起来,计算机仅用于处理一些本地的资料,所以很多国家和地区针对本土的语言设计了编码方案,这种与区域相关的编码统称为ANSI编码(因为都是对ANSI-ASCII码的扩展)。但是他们没有事先商量好怎么相互兼容,而是自己搞自己的,这样就埋下了编码冲突的祸根,比如大陆使用的GB2312编码与台湾使用的Big5编码就有冲突,同样的两个字节,在两种编码方案里表示的是不同的字符,随着互联网的兴起,一个文档里经常会包含多种语言,计算机在显示的时候就遇到麻烦了,因为它不知道这两个字节到底属于哪种编码。

这样的问题在世界上普遍存在,因此重新定义一个通用的字符集,为世界上所有字符进行统一编号的呼声不断高涨。

由此Unicode码应运而生,它为世界上所有字符进行了统一编号,由于它可以唯一标识一个字符,所以字体也只需要针对Unicode码进行设计就行了。但Unicode标准定义的是一个字符集,而没有规定编码方案,也就是说它仅仅定义了一个个抽象的数字与其对应的字符,而没有规定具体怎么存储一串Unicode数字,真正规定怎么存储的是UTF-8、UTF-16、UTF-32等方案,所以带有UTF开头的编码,都是可以直接通过计算和Unicode数值(Code
Point,代码点)进行转换的。
顾名思义,UTF-8就是8位长度为基本单位编码,它是变长编码,用1~6个字节来编码一个字符(因为受Unicode范围的约束,所以实际最大只有4字节);UTF-16是16位为基本单位编码,也是变长编码,要么2个字节要么4个字节;UTF-32则是定长的,固定4字节存储一个Unicode数。

其实我以前一直对Unicode有点误解,在我的印象中Unicode码最大只能到0xFFFF,也就是最多只能表示
2^16
个字符,在仔细看了维基百科之后才明白,早期的UCS-2编码方案确实是这样,UCS-2固定使用两个字节来编码一个字符,因此它只能编码BMP(基本多语言平面,即0×0000-0xFFFF,包含了世界上最常用的字符)范围内的字符。为了要编码Unicode大于0xFFFF的字符,人们对UCS-2编码进行了拓展,创造了UTF-16编码,它是变长的,在BMP范围内,UTF-16与UCS-2完全一致,而BMP之外UTF-16则使用4个字节来存储。

为了方便下面的描述,先交代一下代码单元(Code
Unit)的概念,某种编码的基本组成单位就叫代码单元,比如UTF-8的代码单元为1个字节,UTF-16的代码单元为2个字节,不好解释,但是很好理解。

为了兼容各种语言以及更好的跨平台,Java
String保存的就是字符的Unicode码。它以前使用的是UCS-2编码方案来存储Unicode,后来发现BMP范围内的字符不够用了,但是出于内存消耗和兼容性的考虑,并没有升到UCS-4(即UTF-32,固定4字节编码),而是采用了上面所说的UTF-16,char类型可看作其代码单元。这个做法导致了一些麻烦,如果所有字符都在BMP范围内还没事,若有BMP外的字符,就不再是一个代码单元对应一个字符了,length方法返回的是代码单元的个数,而不是字符的个数,charAt方法返回的自然也是一个代码单元而不是一个字符,遍历起来也变得麻烦,虽然提供了一些新的操作方法,总归还是不方便,而且还不能随机访问。

此外,我发现Java在编译的时候还不会处理大于0xFFFF的Unicode字面量,所以如果你敲不出某个非BMP字符来,但是你知道它的Unicode码,得用一个比较笨的方法来让String存储它:手动计算出该字符的UTF-16编码(四字节),把前两个字节和后两个字节各作为一个Unicode数,然后赋值给String,示例代码如下所示。

public static void main(String[] args) {
        //String str = "";        //我们想赋值这样一个字符,假设我输入法打不出来

        //但我知道它的Unicode是0x1D11E
        //String str = "u1D11E";  //这样写不会识别

        //于是通过计算得到其UTF-16编码 D834 DD1E
        String str = "uD834uDD1E"; //然后这么写

        System.out.println(str);     //成功输出了""
    }

 

Windows系统自带的记事本可以另存为Unicode编码,实际上指的是UTF-16编码。上面说了,主要使用的字符编码都在BMP范围内,而在BMP范围内,每个字符的UTF-16编码值与对应的Unicode数值是相等的,这大概就是微软把它称为Unicode的原因吧。举个例子,我在记事本中输入了”好a“两个字符,然后另存为Unicode
big
endian(高位优先)编码,用WinHex打开文件,内容如下图,文件开头两个字节被称为Byte
Order Mark(字节顺序标记),(FE FF)标识字节序为高位优先,然后(59
7D)正是”好“的Unicode码,(00 61)正是”a“的Unicode码。

澳门新葡亰3522平台游戏 3

有了Unicode码,也还不能立即解决问题,因为首先世界上已经存在了大量的非Unicode标准的编码数据,我们不可能丢弃它们,其次Unicode的编码往往比ANSI编码更占空间,所以从节约资源的角度来说,ANSI编码还是有存在的必要的。所以需要建立一个转换机制,使得ANSI编码可以转换到Unicode进行统一处理,也可以把Unicode转换到ANSI编码以适应平台的要求。

转换方法说起来比较容易,对于UTF系列或者是ISO-8859-1这种被兼容的编码,可以通过计算和Unicode数值直接进行转换(实际可能也是查表),而对于系统遗留下来的ANSI编码,则只能通过查表的方式进行,微软把这种映射表称为Code
Page(代码页),并按编码进行分类编号,比如我们常见的cp936就是GBK的代码页,cp65001就是UTF-8的代码页。下图是微软官网查到的GBK->Unicode映射表(目测不全),同理还应有反向的Unicode->GBK映射表。

澳门新葡亰3522平台游戏 4

有了代码页,就可以很方便的进行各种编码转换了,比如从GBK转换到UTF-8,只需要先按照GBK的编码规则对数据按字符划分,用每个字符的编码数据去查GBK代码页,得到其Unicode数值,再用该Unicode去查UTF-8的代码页(或直接计算),就可以得到对应的UTF-8编码。反过来同理。注意:UTF-8是Unicode的标准实现,它的代码页中包含了所有的Unicode取值,所以任意编码转换到UTF-8,再转换回去都不会有任何丢失。至此,我们可以得出一个结论就是,要完成编码转换工作,最重要的是第一步要成功的转换到Unicode,所以正确选择字符集(代码页)是关键。

理解了转码丢失问题的本质后,我才突然明白JSP的框架为什么要以ISO-8859-1去解码HTTP请求参数,导致我们获取中文参数的时候不得不写这样的语句:

String param = new String(s.getBytes("iso-8859-1"), "UTF-8");

因为JSP框架接收到的是参数编码的二进制字节流,它不知道这究竟是什么编码(或者不关心),也就不知道该查哪个代码页去转换到Unicode。然后它就选择了一种绝对不会产生丢失的方案,它假设这是ISO-8859-1编码的数据,然后查ISO-8859-1的代码页,得到Unicode序列,因为ISO-8859-1是按字节编码的,而且不同于ASCII的是,它对0
~
255空间的每一位都进行了编码,所以任意一个字节都能在它的代码页中找到对应的Unicode,若再从Unicode转回原始字节流的话也就不会有任何丢失。它这样做,对于不考虑其他语言的欧美程序员来说,可以直接用JSP框架解码好的String,而要兼容其他语言的话也只需要转回原始字节流,再以实际的代码页去解码一下就好。

我对Unicode以及字符编码的相关概念阐述完毕,接下来用Java实例来感受一下。

三、实例分析

实例分析

1.转换到Unicode——String构造方法

String的构造方法就是把各种编码数据转换到Unicode序列(以UTF-16编码存储),下面这段测试代码,用来展示Java
String构造方法的应用,实例中都不涉及非BMP字符,所以就不用codePointAt那些方法了。

public class Test {

    public static void main(String[] args) throws IOException {
        //"你好"的GBK编码数据
        byte[] gbkData = {(byte)0xc4, (byte)0xe3, (byte)0xba, (byte)0xc3};
        //"你好"的BIG5编码数据
        byte[] big5Data = {(byte)0xa7, (byte)0x41, (byte)0xa6, (byte)0x6e};

        //构造String,解码为Unicode

        String strFromGBK = new String(gbkData, "GBK");
        String strFromBig5 = new String(big5Data, "BIG5");

        //分别输出Unicode序列

        showUnicode(strFromGBK);
        showUnicode(strFromBig5);
    }

    public static void showUnicode(String str) {
        for (int i = 0; i < str.length(); i++) {
            System.out.printf("\u%x", (int)str.charAt(i));
        }
        System.out.println();
    }
}

运行结果如下图

澳门新葡亰3522平台游戏 5

从结果可以发现,只要指定了正确的字符集(代码页),String就可以解码出正确的Unicode,最后可以试试println(“u4f60u597d”),输出的就是“你好”。

1.转换到Unicode——String构造方法

String的构造方法就是把各种编码数据转换到Unicode序列(以UTF-16编码存储),下面这段测试代码,用来展示Java
String构造方法的应用,实例中都不涉及非BMP字符,所以就不用codePointAt那些方法了。

public class Test {

    public static void main(String[] args) throws IOException {
        //"你好"的GBK编码数据
        byte[] gbkData = {(byte)0xc4, (byte)0xe3, (byte)0xba, (byte)0xc3};
        //"你好"的BIG5编码数据
        byte[] big5Data = {(byte)0xa7, (byte)0x41, (byte)0xa6, (byte)0x6e};

        //构造String,解码为Unicode

        String strFromGBK = new String(gbkData, "GBK");
        String strFromBig5 = new String(big5Data, "BIG5");

        //分别输出Unicode序列

        showUnicode(strFromGBK);
        showUnicode(strFromBig5);
    }

    public static void showUnicode(String str) {
        for (int i = 0; i < str.length(); i++) {
            System.out.printf("\u%x", (int)str.charAt(i));
        }
        System.out.println();
    }
}

 

 

运行结果如下图

澳门新葡亰3522平台游戏 6

从结果可以发现,只要指定了正确的字符集(代码页),String就可以解码出正确的Unicode,最后可以试试println(“u4f60澳门新葡亰3522平台游戏,u597d”),输出的就是“你好”。

2.Unicode转换到各种编码——getBytes

String拥有了Unicode序列,想要转换到其它编码就易如反掌了,根据你参数指定的字符集,去相应的代码页查找就可以转换过去了,当然如果该字符集不支持某字符(也就是没有这条Unicode记录),那就会导致编码丢失,再也不能还原到原来的Unicode序列了。

这里,我们和第1节的做法相反,我们把Unicode序列转换到其它各种编码,如下所示。

public class Test {

    public static void main(String[] args) throws IOException {
        //字符串"你好"
        String str = "u4f60u597d";

        //转换到各种编码

        showBytes(str, "GBK");
        showBytes(str, "BIG5");
        showBytes(str, "UTF-8");
    }

    public static void showBytes(String str, String charset) throws IOException {
        for (byte b : str.getBytes(charset))
            System.out.printf("0x%x ", b);
        System.out.println();
    }
}

运行结果如下图

澳门新葡亰3522平台游戏 7

可以发现,由于String掌握了Unicode码,要转换到其它编码so easy!

2.Unicode转换到各种编码——getBytes

String拥有了Unicode序列,想要转换到其它编码就易如反掌了,根据你参数指定的字符集,去相应的代码页查找就可以转换过去了,当然如果该字符集不支持某字符(也就是没有这条Unicode记录),那就会导致编码丢失,再也不能还原到原来的Unicode序列了。

这里,我们和第1节的做法相反,我们把Unicode序列转换到其它各种编码,如下所示。

public class Test {

    public static void main(String[] args) throws IOException {
        //字符串"你好"
        String str = "u4f60u597d";

        //转换到各种编码

        showBytes(str, "GBK");
        showBytes(str, "BIG5");
        showBytes(str, "UTF-8");
    }

    public static void showBytes(String str, String charset) throws IOException {
        for (byte b : str.getBytes(charset))
            System.out.printf("0x%x ", b);
        System.out.println();
    }
}

 

 

运行结果如下图

澳门新葡亰3522平台游戏 8

可以发现,由于String掌握了Unicode码,要转换到其它编码so easy!