Heim > php教程 > php手册 > 从Java String实例来理解ANSI、Unicode、BMP、UTF等编码概念

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

WBOY
Freigeben: 2016-06-01 09:46:44
Original
1904 Leute haben es durchsucht

前两天同学与我谈起一个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,示例代码如下所示。

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

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

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

        System.out.println(str);     //成功输出了""
    }</code>
Nach dem Login kopieren

 

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码。

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

有了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映射表。

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

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

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

<code class="language-java">String param = new String(s.getBytes("iso-8859-1"), "UTF-8");</code>
Nach dem Login kopieren

因为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那些方法了。

<code class="language-java">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 </code>
Nach dem Login kopieren

运行结果如下图

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

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

 

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

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

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

<code class="language-java">public class Test {

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

        //转换到各种编码

        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();
    }
}</code>
Nach dem Login kopieren

运行结果如下图

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

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

 

3.以Unicode为桥梁,实现编码互转

有了上面两部分的基础,要实现编码互转就很简单了,只需要把他们联合使用就可以了。先new String把原编码数据转换为Unicode序列,再调用getBytes转到指定的编码就OK。

比如一个很简单的GBK到Big5的转换代码如下

<code class="language-java">public static void main(String[] args) throws UnsupportedEncodingException {
        //假设这是以字节流方式从文件中读取到的数据(GBK编码)
        byte[] gbkData = {(byte) 0xc4, (byte) 0xe3, (byte) 0xba, (byte) 0xc3};

        //转换到Unicode
        String tmp = new String(gbkData, "GBK");

        //从Unicode转换到Big5编码
        byte[] big5Data = tmp.getBytes("Big5");

        //后续操作……
    }</code>
Nach dem Login kopieren

 

4.编码丢失问题

上面已经解释了,JSP框架采用ISO-8859-1字符集来解码的原因。先用一个例子来模拟这个还原过程,代码如下

<code class="language-java">public class Test {

    public static void main(String[] args) throws UnsupportedEncodingException {
        //JSP框架收到6个字节的数据
        byte[] data = {(byte) 0xe4, (byte) 0xbd, (byte) 0xa0, (byte) 0xe5, (byte) 0xa5, (byte) 0xbd};
        //打印原始数据
        showBytes(data);
        //JSP框架假设它是ISO-8859-1的编码,生成一个String对象
        String tmp = new String(data, "ISO-8859-1");

        //**************JSP框架部分结束********************

        //开发者拿到后打印它发现是6个欧洲字符,而不是预期的"你好"
        System.out.println("  ISO解码的结果:" + tmp);

        //因此首先要得到原始的6个字节的数据(反查ISO-8859-1的代码页)
        byte[] utfData = tmp.getBytes("ISO-8859-1");

        //打印还原的数据
        showBytes(utfData);

        //开发者知道它是UTF-8编码的,因此用UTF-8的代码页,重新构造String对象
        String result = new String(utfData, "UTF-8");
        //再打印,正确了!
        System.out.println("  UTF-8解码的结果:" + result);
    }

    public static void showBytes(byte[] data) {
        for (byte b : data)
            System.out.printf("0x%x ", b);
        System.out.println();
    }
}</code>
Nach dem Login kopieren

运行结果如下,第一次输出是不正确的,因为解码规则不对,也查错了代码页,得到的是错误的Unicode。然后发现通过错误的Unicode反查ISO-8859-1代码页还能完美的还原数据。

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

然后我们尝试把ISO-8859-1替换为ASCII,结果就会变成这样子

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

这是因为,ASCII虽然也是每字节对应一个字符,但是它只对0~127这个空间进行了编码,也就是说每个字节的最大值只能为0x7F,而上面的6个字节全部都大于这个数值,因此在ASCII的代码页中是找不到这6个字节的,于是Java就搞了一个缺省值。我用如下的代码测试发现,当通过编码数据在代码页中查不到对应的Unicode时,就返回缺省值\ufffd(对应图中第一种问号),反过来,当通过Unicode在代码页中查不到对应的编码数据时,就返回缺省值0x3f(ASCII,对应图中第二种问号)。由此,这个输出结果也就可以解释清楚了。

<code class="language-java">public static void main(String[] args) throws IOException {
        //输出结果全为\ufffd
        byte[] data = {(byte) 0x80};
        showUnicode(new String(data, "UTF-8"));
        showUnicode(new String(data, "GBK"));
        showUnicode(new String(data, "Big5"));

        //输出结果全为0x3f
        String str = "\uccdd";
        showBytes(str, "GBK");
        showBytes(str, "BIG5");
        showBytes(str, "ISO-8859-1");
    }</code>
Nach dem Login kopieren

 

 

5.Java源文件的编码问题

这就是开头所提到的那个问题,把问题描述一下先。就如下这么一小段代码,源文件使用UTF-8编码保存。(注意别用Windows的记事本,因为它会在UTF-8文件最前面加入一个3字节的BOM头,而很多程序都不兼容这一点)

<code class="language-java">public class Test {

    public static void main(String[] args) {
        System.out.println("中");
    }
}</code>
Nach dem Login kopieren

然后在Windows中使用默认参数编译该文件(系统区域设置为简体中文,即默认使用GBK字符集解码),然后会得到如下错误

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

这不是重点,重点如果把“中”换成“中国”,编译就会成功,运行结果如下图。另外进一步可发现,中文字符个数为奇数时编译失败,偶数时通过。这是为什么呢?下面详细分析一下。

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

因为Java String内部使用的是Unicode,所以在编译的时候,编译器就会对我们的字符串字面量进行转码,从源文件的编码转换到Unicode(维基百科说用的是与UTF-8稍微有点不同的编码)。编译的时候我们没有指定encoding参数,所以编译器会默认以GBK方式去解码,对UTF-8和GBK有点了解的应该会知道,一般一个中文字符使用UTF-8编码需要3个字节,而GBK只需要2个字节,这就能解释为什么字符数的奇偶性会影响结果,因为如果2个字符,UTF-8编码占6个字节,以GBK方式来解码恰好能解码为3个字符,而如果是1个字符,就会多出一个无法映射的字节,就是图中问号的地方。

再具体一点的话,源文件中“中国”二字的UTF-8编码是 e4 b8 ad e5 9b bd,编译器以GBK方式解码,3个字节对分别查cp936得到3个Unicode值,分别是6d93 e15e 6d57,对应结果图中的三个奇怪字符。如下图所示,编译后这3个Unicode在.class文件中实际以类UTF-8编码存储,运行的时候,JVM中存储的就是Unicode,然而最终输出时,还是会编码之后传递给终端,这次约定的编码就是系统区域设置的编码,所以如果终端编码设置改了,还是会乱码。我们这里的e15e在Unicode标准中并没有定义相应的字符,所以在不同平台不同字体下显示会有所不同。

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

可以想象,如果反过来,源文件以GBK编码存储,然后骗编译器说是UTF-8,那基本上是无论输入多少个中文字符都无法编译通过了,因为UTF-8的编码很有规律性,随意组合的字节是不会符合UTF-8编码规则的。

当然,要使编译器能正确的把编码转换到Unicode,最直接的方法还是老老实实告诉编译器源文件的编码是什么。

 

四、总结

经过这次收集整理和实验,了解了很多与编码相关的概念,也熟悉了编码转换的具体过程,这些思想可以推广到各种编程语言去,实现原理都类似,所以我想以后再遇到这类问题,应该不会再不知所以然了。

Verwandte Etiketten:
Quelle:php.cn
Erklärung dieser Website
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn
Beliebte Empfehlungen
Beliebte Tutorials
Mehr>
Neueste Downloads
Mehr>
Web-Effekte
Quellcode der Website
Website-Materialien
Frontend-Vorlage