美文网首页
Java 流和编码乱码问题

Java 流和编码乱码问题

作者: 爱秋刀鱼的猫 | 来源:发表于2019-07-22 14:18 被阅读0次

    最近一直在被编码问题困扰。觉得这是我“职业生涯”里过不去的坎儿,算是我的梦魇。一想到只要我搬一天的砖,它就可能折磨我一次,我决定好好看一下。于是我拿起了《Java核心技术》这本书,看是翻起了第2章 输入与输出。结合网上的一些教程,然后以我的理解,解决了我自己在编程中遇到的一个乱码问题。不像之前是边百度,边尝试(有种神农尝百草的意思)各种帖子上写的方法,碰运气解决,这一次我是有点自我意识在改bug的(骄傲脸)。所以我打算在博采众长之后,把我这几天学到的东西整理一下,可能中间还是有很多问题,或者是我理解不对的,还需要大家帮忙指出来,我再改正。来吧,开始打脸(委屈脸)。

    什么是输入流?什么是输出流?

    这是首先需要解决的问题。其实就是明白自己的定位。我觉得网上很多的教程其实是有问题的,因为他们一上来就是把《Java核心技术》这本书上的概念再念一遍,但是看不懂得还是看不懂。因为他们忽略了“相对”和“绝对”的概念,所以我觉得他们是在耍流氓。

    在Java中,“流”根据其流动的方向是可以分为“输入流”和“输出流”的。那这个“方向”怎么定义。这是重点,但是很多人都不提:)
    今天我就要大声告诉你,
    输入流,输出流是以程序为参考点来说的。
    输入流,输出流是以程序为参考点来说的。
    输入流,输出流是以程序为参考点来说的。
    输入流:就是给程序提供数据的流,程序可以从输入流里获取自己想要的数据。
    输出流:是程序要向其写入数据的流,也就是数据的目的地。
    我觉得知道这一点,其实就知道是使用InputStream,还是OutputStream了。比如,需要从文件A读入数据,那就new一个InputStream对象,然后调用read()方法。反之,要向文件B写入数据,就new一个OutputStream对象,然后调用write()方法。
    我讲完了。
    emmm,是不是觉得我就是一个“水王”。但是我觉得这是我今天学到的最有用的知识了。如果还需要补充一点的话,就是“流”与“流”之间如何传递数据,或者更确切一点说就是,之间的“物质”是什么?

    答案是:字节流。(心里默念一遍:一个字节等于8bit)

    但是,这个字节流到底是怎么得到的?我的问题是:我们程序白纸黑字写的“Hello,程序媛!”是怎么变成字节的呢?字节流又是怎么变成我们认识的文字的呢?

    自问自答:编码 和 解码

    嗯,应该知道我接下去要说的是什么了吧,就是乱码问题了。

    Java字符的编码与乱码问题

    我觉得知乎上的这篇文章写的超级好。值得我们每一个被“乱码”问题折磨的人。https://zhuanlan.zhihu.com/p/25435644
    虽然他写了,但我还是想再复刻一遍。(人类的本质是复读机)

    1、一幅图和四个概念

    [图片上传失败...(image-60d448-1563776264791)]

    字符有三种形态:形状(显示在显示设备上)、数字(运行于JVM中,Java统一为unicode编码)和字节数组(不同的字符集有不同的映射方案)。
    字符集合(Character set) :是一组形状的集合。例如所有汉字的集合,发明于公元前,发明者是仓颉。它体现了字符的“形状”,它与计算机、编码等无关。
    编码字符集(Coded character set) :是一组字符对应的编码(即数字),为字符集合中的每一个字符给予一个数字。例如最早的编码字符集ASCII,发明于1967年。再例如Java使用的unicode,发明于1994年(持续更新中)。由于编码字符集为每一个字符赋予一个数字,因此在java内部,字符可以认为就是一个16位的数字,因此以下方式都可以给字符赋值:

    char c =‘中’
    char c = 0x4e2d
    char c = 20013
    

    字符编码方案(Character-encoding schema) :将字符编码(数字)映射到一个字节数组的方案,因为在磁盘里,所有信息都是以字节的方式存储的。因此Java的16位字符必须转换为一个字节数组才能够存储。例如UTF-8字符编码方案,它可以将一个字符转换为1、2、3或者4个字节。
    一般认为,编码字符集和字符编码方案合起来被称之为 字符集(Charset) ,这是一个术语,要和前面的字符集合(Character set)区分开。

    2、类型之间的转化

    2.1 从数字到形状

    就是说从JVM中的数字,变为屏幕上显示的文字,这一转化过程是在字体库的帮助下完成的,所以无需我们操心,也不会出错,只要你给的数字是对的,你就能得到你想要的数据,所以这一转化知道就行。

    2.2 从数字到字节组——编码

    这是我们今天的重点。
    如图所示,从JVM中的数字转化为字节数组,也就是我们心心念念的“物质”,这个过程就是“编码”。经过“编码”,我们就能得到可以传输,或者便于存储的字节流。JVM上的同一个数字,比如0x4e2d,采用不同的字符集进行编码,能得到不同的字节数组。就如图中可以看出,采用UTF-8的编码得到的结果是e4 b8 ad;采用GBK编码得到的结果是d6 d0;采用UTF-16编码得到的是fe ff 4e 2d。有兴趣的同学,其实还是可以想想,这些数字是怎么得到的。而我就是这样一个好奇且好学的宝宝,我想知道他有没有骗我,所以我查了一下资料。其中这篇文章,我觉得还是挺良心的:https://www.jianshu.com/p/35f5f7d07732
    比如就UTF-8这种编码方式来举个吧:

    UTF-8的编码规则很简单,只有二条:
    1、对于单字节的符号,字节的第一位设为0,后面7位为这个符号的unicode码。因此对于英语字母,UTF-8编码和ASCII码是相同的。
    2、对于n字节的符号(n>1),第一个字节的前n位都设为1,第n+1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的unicode码。

    看文字很费解,上图:
    【图略】
    就拿我们的“中”字而言,它在JVM的数字是0x 4e 2d,属于上面Unicode字符中的第三种情况,所以就可以把转换的16个二进制依次放入上述的x中。我利用在线的二进制转化武器,可以得到e4 b8 ad的结果,这就可以看出这位作者是真的很良心,糟老头也不都是坏的。
    至于其他的编码方式,想验证的可以去看看规则然后动手试一下。

    上面那么多看似很高端的东西,其实看不懂也可以不用看懂,我提一下就是为了zhuangbility,因为我们平时写代码完全是无感知的。了解了最多就是心里踏实一点,不了解知道怎么用就好。但是,你要确保你真的会用,不然你的老板会不高兴的。
    编码的例子代码如下:
    第一种方法,使用String的getBytes方法:

    private static byte[] encoding1(String str, String charset) throws UnsupportedEncodingException {
        return str.getBytes(charset);
    }
    

    第二种方法,使用Charset的encode方法:

    private static byte[] encoding2(String str, String charset) {
            Charset cset = Charset.forName(charset);
            ByteBuffer byteBuffer = cset.encode(str);
            byte[] bytes = new byte[byteBuffer.remaining()];
            byteBuffer.get(bytes);
            return bytes;
    }
    

    实现的方式千千万,但是我们一定要抓到重点:编码得到的什么结果。就是这玩意儿: byte[] 。对,就是我们需要的字节流,就是我们需要的“物质”。

    2.3 从字节数组到数字——解码

    在完成了一系列操作以后,你还是需要让别人知道你在想什么,最好的方式就是文字,我们大家能看得到的东西,而字节数组这东西,太过于抽象,所以我们需要把它变为一个数字,这个转化过程就是解码。解码就是把从磁盘或者网络上得到的信息,转换为字符或字符串。
    解码与编码最大的区别是,解码难。难在哪里。就是你不知道或者你没有意识去了解,你拿到的字节之前是怎么编码的。就好像你不知道你现在身边的人之前遇到过谁。所以解码时一定要指定字符集,否则将会使用默认的字符集进行解码。如果使用了错误的字符集,则会出现乱码。
    解码的例子代码如下:
    第一种方法,使用String的构造函数:

    private static String decoding1(byte[] bytes,String charset) throws UnsupportedEncodingException {
            String str = new String(bytes, charset);
            return str;
        }
    

    第二种方法,使用Charset的decode方法:

     private static String decoding2(byte[] bytes, String charset) {
            Charset cset = Charset.forName(charset);
            ByteBuffer buffer = ByteBuffer.wrap(bytes);
            CharBuffer charBuffer = cset.decode(buffer);
            return charBuffer.toString();
        }
    

    3、 默认的字符集

    乱码问题是因为我们在编码和解码的过程中,采用了不一样的字符集。有时候如果我们没有指明编码和解码的方式就会采用默认的字符集,如果我们不知道什么是默认的字符集,就会有可能出现乱码的问题。Java的默认字符集,可以在两个地方设定,一是执行java程序时使用-D file.encoding参数指定,例如 -D file.encoding=UTF-8 就指定默认字符集是UTF-8。二是在程序执行时使用Properties进行指定,如下:

    private static void setEncoding(String charset) {
        Properties properties = System.getProperties();
        properties.put("file.encoding",charset);
        System.out.println(properties.get("file.encoding"));
    }
    

    注意,这两种方法如果同时使用,则程序开始时使用参数指定的字符集,在Properties方法后使用Properties指定的字符集。
    如果这两种方法都没有使用,则使用操作系统默认的字符集。例如中文版windows 7的默认字符集是GBK。
    默认字符集的优先级如下:
    1.程序执行时使用Properties指定的字符集;
    2.java命令的-Dfile.encoding参数指定的字符集;
    3.操作系统默认的字符集;
    4.JDK中默认的字符集,我跟踪了JDK1.8的源代码,发现其默认字符集指定为ISO-8859-1

    4、 乱码

    从上述章节可知,字符的形态有三种,分别是“形状”、“数字”和“字节”。字符的三种形态之间的转换也有三类:从数字到形状,从数字到字节(编码),从字节到数字(解码)。
    从数字到形状不会产生乱码,乱码就产生在编码和解码的时候。仔细想来,编码也是不会产生乱码的,因为从数字到字节(指定某个字符集)一定能够转换成功,即使某字符集中不包含该数字,它也会用指定的字节来代替,并在转换时给出指示。
    如此一来,乱码只会产生在解码时:例如使用某字符集A编码的字节,使用字符集B来进行解码,而A和B并不兼容。这样一来,解码产生的数字(字符编码)就是错误的,那么它显示出来也是错误的。

    相关文章

      网友评论

          本文标题:Java 流和编码乱码问题

          本文链接:https://www.haomeiwen.com/subject/usmilctx.html