深入了解java中的编码和字符集

未分类 一条评论


参考资料:
Java中的字符集编码入门
java编译器编码和JVM编码问题?

Unicode是一个字符集,就好像一个字典一样,收录了全世界的文字啥的,英文是charset;

ascii、gbk、utf-8、utf-16叫字符集编码,英文是encoding,例如utf-8和utf-16编码是Unicode字符集的实现,规定了字符在计算机中的具体编码规则,也就是二进制到底是什么样的,在计算机中的意思是,可以是在硬盘文件中,也可以是内存里。

昨天在看阮一峰大侠的es6,突然看到了es6对string的扩展,介绍了string.codepointat()之类的方法,还涉及到了编码方式,瞬间又懵逼了,编码这个玩意我看了一遍又一遍,每次都觉得懂了些,可下次再遇到编码的问题还是一头雾水,理解的还是太浅了啊!

所以我就好奇,在java里面这个编码是咋地个处理法?平时用idea coding的时候,文件都是用utf-8保存的,也就是说java源代码是用utf-8进行编码的,如下图。

这里写图片描述

那么javac在编译.java文件的时候也要按照utf-8编码方式进行读取,否则就有可能出现乱码。

那javac为什么知道这个文件是utf-8的呢,它特么肯定知不道,因为文件的编码方式并不储存在文件里,文件中存储的是内容的二进制表示。正常来说,如果开一个bash,运行javac Test.java,那么他使用的是操作系统默认的编码方式进行读取Test.java,对于windows中文系统来说一般是gbk,这个值在java代码中可以用System.getProperty(“file.encoding”)获取 。所以如果一个源文件的编码方式是utf-8,然后你直接javac它,绝逼会报下面的错误(如果你是win的话~):

public class test {
    public static void main(String args[]){
        char cchar = '汉';
        System.out.format("%x",(short)cchar);
    }
}

这里写图片描述

但是为啥我在idea里面写完,然后在idea里面运行,毛事没有呢?因为idea给咱处理了,指定了读取源文件的编码方式,我估计它是这么指定的:javac -encoding utf-8 test.java(记不住的话可以javac -help查看一下),这样的话System.getProperty(“file.encoding”)也会变成utf-8的。 编译完成,生成的.class文件默认应该是utf-8编码的。

这样javac读入.java并且生成.class文件算是清楚了(真的吗- -),但是还有一点我特么特别好奇也特别生气,就是如果源文件有语法错误,javac会将错误信息重定向到bash中(不知道这个说法对不对~),也就是javac的输出,总是乱码,我就想这个输出到底是什么编码的???

我是win7,开的git bash,系统默认编码是gbk,.java源文件是utf-8,bash解析输入并显示在屏幕上时使用的编码是utf-8。

然后我故意弄个语法错误,然后 javac -encoding utf-8 test.java 出现下面这个jb玩意(说鸡不说吧,文明你我他~)

这里写图片描述

看来输出错误信息的时候bash用utf-8解码显示有乱码啊!

把bash编码换成gbk试试

这里写图片描述

然后就正常显示了~

这里写图片描述

这说明啥呢?说明javac在把错误信息重定向到bash时,使用的操作系统默认的编码方式对数据转换后进行传输,这也理所应当,因为这里是一个边界,javac和操作系统打交道的边界,不管你javac用啥编码,如果你想把一些信息给操作系统,然后呈现给用户,那么你就必须尊重人家操作系统的编码方式,并且对输入数据进行正确的编码,要不操作系统怎么会正常显示输出呢?不过这个输出到操作系统的编码方式一开始我在System.getProperties()中没找到,只找到了一个sun.jnu.encoding是gbk,然后网上一搜,这个属性是影响文件名创建的编码的- – 。

后来我又实验了一下,发现javac -enciding utf-8 test.java设置的编码只是说明javac编译器读取.java文件时使用的编码方式,影响不到System.getProperty(“file.encoding”)这个属性。原来这个属性是启动jvm时可以设置的,默认是操作系统的编码方式。java -Dfile.encoding=xxx test 进行设置,而且这个属性就是java程序运行时与操作系统打交道时使用的编码方式!当然这个属性是影响不到javac的,因为javac要在java命令之前运行~~

java程序运行起来之前的编码就说到这里吧,接下来说说jvm中的编码。

jvm内部使用的字符集编码为utf-16,也就是字符在内存中的储存方式为utf-16。对,就是16不是8,- -。现在utf-8这么火这么流行这么普及为啥不用8呢?好像是之前sun被unicode联盟坑了~具体咋回事可以左转bi乎。就比如说一个String吧 ,实际的内容是存在String类中private final char value[]数组中的,这个char数组存的东东就是经过utf-16编码的数据!嗯,就是这样。感觉有些懵逼,现在企业开发一般全站都特么用utf-8,这咋jvm你内部自己用个utf-16呢,尴尬。用就用吧,我也没办法- -。一开始java用utf16,有个原因好像是它的编码方式是定长的,就用俩字节表示字符,java正好可以用一个char表示一个字符,完美啊。但天有不测风云,俩字节最多表示65536个字符,可全世界的语言里的字符不止这么多啊,光™汉语都不止这么多,于是utf-16又扩充了,utf-16就用2字节或者4个字节表示字符了,跟™utf-8一样也成了变长编码了- -。不过我就想不明白了,如果有一串字节,用它来表示一个字符串,那™怎么解析啊,到底是读取2字节作为一个字符还是读取4字节作为一个字符啊,咋™区分呢?其实是这样的,不知道是unicode联盟还是sun(估计是unicode),整了一个规定,unicode码空间U+0000到U+FFFF为BMP(Basic Multilingual Plane基本多语言面),U+10000之后的码空间对应补充字符,然后为了正确读取2字节字符(也就是bmp中的)和4字节字符(补充字符),规定U+D800到U+DFFF在bmp中不对应字符,让补充字符使用这一段。两个char 组成了surrogate pair,第一个char成为高代理部分(high-surrogates range uD800到uDBFF ,1024个),第二个char叫低代理部分,uDC00到uDFFF,也是1024个,1024*1024也就是1048576个补充字符,加上bmp65536-2048个字符,一共1112064个。我这里说的乱七八糟的,大家可以来这里看,这篇文章讲的老带劲了http://www.360doc.cn/article/9470897_205152817.html

上面扯了这么半天,其实涉及到一个代码点(Code Point)和代码单元(Code Unit)的概念问题.

(引用自上面网址) 代码点(Code Point)就是指Unicode中为字符分配的编号,一个字符只占一个代码点,例如我们说到字符“汉”,它的代码点是U+6C49.代码单元(Code Unit)则是针对编码方法而言,它指的是编码方法中对一个字符编码以后所占的最小存储单元。例如UTF-8中,代码单元是一个字节,因为一个字符可以被编码为1个,2个或者3个4个字节;在UTF-16中,代码单元变成了两个字节(就是一个char),因为一个字符可以被编码为1个或2个char(你找不到比一个char还小的UTF-16编码的字符,嘿嘿)。说得再罗嗦一点,一个字符,仅仅对应一个代码点,但却可能有多个代码单元(即可能被编码为2个char)。

java类库中有的方法是跟代码点打交道的,有的是跟代码单元打交道的,java中的代码点就是指的Unicode字符集的代码点了,代码单元自然指的是utf-16的代码单元。比如String.length( )返回的就是utf-16代码单元的数量,看以下源码:

    /**
     * Returns the length of this string.
     * The length is equal to the number of <a href="Character.html#unicode">Unicode
     * code units</a> in the string.
     *
     * @return  the length of the sequence of characters represented by this
     *          object.
     */
    public int length() {
        return count;
    }

也就是说,对于BMP中的字符来说,length可以代表字符的个数,但对于含有补充字符的字符串来说,length就不能反映出字符串中含有字符的真实个数了:

这里写图片描述

这里写图片描述

这个古怪的汉字是补充字符,在utf-16编码中用4个字节,也就是两个char来表示,看到了吧,length( )返回的是 6 哦~~ 所以在一些用户注册的时候判断用户名长度,直接用length判断其实是有些小问题的,当然正常人是不会用补充字符的汉字的- – 另外在京东 淘宝注册的时候一个汉字被算作2个字符- -,而且不支持 ‘’ 这种补充字符 – – 要真想求出字符串的代码点的数量也就是我们正常人所理解的字符的数量,可以用str.codePointCount(0,str.length-1)方法。

再比如对于str.charAt( int index) 这种index也是指的代码单元,比如返回index为0的char,也就是代码单元。

再比如 Character.toChars(0x2F81A) 这个方法的参数为unicode的代码点(code point),返回一个utf-16编码的char数组。

再比如str.getBytes()返回一个byte数组,这个byte数组的编码为操作系统默认的编码,也就是file.encoding对应的编码,这个方法也可以接受具体的编码作为参数来生成对应编码的byte数组。

等等等等,String类和Character类中有很多很多方法涉及代码点和代码单元,有时间的时候可以阅读以下源码的注释,了解一下。

1条评论

1
12额2东方分 says: 回复

123

发表评论

邮箱地址不会被公开。 必填项已用*标注

昵称 *