Java 流和编码乱码问题

最近一直在被编码问题困扰。觉得这是我“职业生涯”里过不去的坎儿,算是我的梦魇。一想到只要我搬一天的砖,它就可能折磨我一次,我决定好好看一下。于是我拿起了《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并不兼容。这样一来,解码产生的数字(字符编码)就是错误的,那么它显示出来也是错误的。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容