最近一直在被編碼問題困擾。覺得這是我“職業(yè)生涯”里過不去的坎兒,算是我的夢(mèng)魘。一想到只要我搬一天的磚,它就可能折磨我一次,我決定好好看一下。于是我拿起了《Java核心技術(shù)》這本書,看是翻起了第2章 輸入與輸出。結(jié)合網(wǎng)上的一些教程,然后以我的理解,解決了我自己在編程中遇到的一個(gè)亂碼問題。不像之前是邊百度,邊嘗試(有種神農(nóng)嘗百草的意思)各種帖子上寫的方法,碰運(yùn)氣解決,這一次我是有點(diǎn)自我意識(shí)在改bug的(驕傲臉)。所以我打算在博采眾長(zhǎng)之后,把我這幾天學(xué)到的東西整理一下,可能中間還是有很多問題,或者是我理解不對(duì)的,還需要大家?guī)兔χ赋鰜?,我再改正。來吧,開始打臉(委屈臉)。
什么是輸入流?什么是輸出流?
這是首先需要解決的問題。其實(shí)就是明白自己的定位。我覺得網(wǎng)上很多的教程其實(shí)是有問題的,因?yàn)樗麄円簧蟻砭褪前选禞ava核心技術(shù)》這本書上的概念再念一遍,但是看不懂得還是看不懂。因?yàn)樗麄兒雎粤恕跋鄬?duì)”和“絕對(duì)”的概念,所以我覺得他們是在耍流氓。
在Java中,“流”根據(jù)其流動(dòng)的方向是可以分為“輸入流”和“輸出流”的。那這個(gè)“方向”怎么定義。這是重點(diǎn),但是很多人都不提:)
今天我就要大聲告訴你,
輸入流,輸出流是以程序?yàn)閰⒖键c(diǎn)來說的。
輸入流,輸出流是以程序?yàn)閰⒖键c(diǎn)來說的。
輸入流,輸出流是以程序?yàn)閰⒖键c(diǎn)來說的。
輸入流:就是給程序提供數(shù)據(jù)的流,程序可以從輸入流里獲取自己想要的數(shù)據(jù)。
輸出流:是程序要向其寫入數(shù)據(jù)的流,也就是數(shù)據(jù)的目的地。
我覺得知道這一點(diǎn),其實(shí)就知道是使用InputStream,還是OutputStream了。比如,需要從文件A讀入數(shù)據(jù),那就new一個(gè)InputStream對(duì)象,然后調(diào)用read()方法。反之,要向文件B寫入數(shù)據(jù),就new一個(gè)OutputStream對(duì)象,然后調(diào)用write()方法。
我講完了。
emmm,是不是覺得我就是一個(gè)“水王”。但是我覺得這是我今天學(xué)到的最有用的知識(shí)了。如果還需要補(bǔ)充一點(diǎn)的話,就是“流”與“流”之間如何傳遞數(shù)據(jù),或者更確切一點(diǎn)說就是,之間的“物質(zhì)”是什么?
答案是:字節(jié)流。(心里默念一遍:一個(gè)字節(jié)等于8bit)
但是,這個(gè)字節(jié)流到底是怎么得到的?我的問題是:我們程序白紙黑字寫的“Hello,程序媛!”是怎么變成字節(jié)的呢?字節(jié)流又是怎么變成我們認(rèn)識(shí)的文字的呢?
自問自答:編碼 和 解碼
嗯,應(yīng)該知道我接下去要說的是什么了吧,就是亂碼問題了。
Java字符的編碼與亂碼問題
我覺得知乎上的這篇文章寫的超級(jí)好。值得我們每一個(gè)被“亂碼”問題折磨的人。https://zhuanlan.zhihu.com/p/25435644
雖然他寫了,但我還是想再復(fù)刻一遍。(人類的本質(zhì)是復(fù)讀機(jī))
1、一幅圖和四個(gè)概念
[圖片上傳失敗...(image-60d448-1563776264791)]
字符有三種形態(tài):形狀(顯示在顯示設(shè)備上)、數(shù)字(運(yùn)行于JVM中,Java統(tǒng)一為unicode編碼)和字節(jié)數(shù)組(不同的字符集有不同的映射方案)。
字符集合(Character set) :是一組形狀的集合。例如所有漢字的集合,發(fā)明于公元前,發(fā)明者是倉頡。它體現(xiàn)了字符的“形狀”,它與計(jì)算機(jī)、編碼等無關(guān)。
編碼字符集(Coded character set) :是一組字符對(duì)應(yīng)的編碼(即數(shù)字),為字符集合中的每一個(gè)字符給予一個(gè)數(shù)字。例如最早的編碼字符集ASCII,發(fā)明于1967年。再例如Java使用的unicode,發(fā)明于1994年(持續(xù)更新中)。由于編碼字符集為每一個(gè)字符賦予一個(gè)數(shù)字,因此在java內(nèi)部,字符可以認(rèn)為就是一個(gè)16位的數(shù)字,因此以下方式都可以給字符賦值:
char c =‘中’
char c = 0x4e2d
char c = 20013
字符編碼方案(Character-encoding schema) :將字符編碼(數(shù)字)映射到一個(gè)字節(jié)數(shù)組的方案,因?yàn)樵诖疟P里,所有信息都是以字節(jié)的方式存儲(chǔ)的。因此Java的16位字符必須轉(zhuǎn)換為一個(gè)字節(jié)數(shù)組才能夠存儲(chǔ)。例如UTF-8字符編碼方案,它可以將一個(gè)字符轉(zhuǎn)換為1、2、3或者4個(gè)字節(jié)。
一般認(rèn)為,編碼字符集和字符編碼方案合起來被稱之為 字符集(Charset) ,這是一個(gè)術(shù)語,要和前面的字符集合(Character set)區(qū)分開。
2、類型之間的轉(zhuǎn)化
2.1 從數(shù)字到形狀
就是說從JVM中的數(shù)字,變?yōu)槠聊簧巷@示的文字,這一轉(zhuǎn)化過程是在字體庫的幫助下完成的,所以無需我們操心,也不會(huì)出錯(cuò),只要你給的數(shù)字是對(duì)的,你就能得到你想要的數(shù)據(jù),所以這一轉(zhuǎn)化知道就行。
2.2 從數(shù)字到字節(jié)組——編碼
這是我們今天的重點(diǎn)。
如圖所示,從JVM中的數(shù)字轉(zhuǎn)化為字節(jié)數(shù)組,也就是我們心心念念的“物質(zhì)”,這個(gè)過程就是“編碼”。經(jīng)過“編碼”,我們就能得到可以傳輸,或者便于存儲(chǔ)的字節(jié)流。JVM上的同一個(gè)數(shù)字,比如0x4e2d,采用不同的字符集進(jìn)行編碼,能得到不同的字節(jié)數(shù)組。就如圖中可以看出,采用UTF-8的編碼得到的結(jié)果是e4 b8 ad;采用GBK編碼得到的結(jié)果是d6 d0;采用UTF-16編碼得到的是fe ff 4e 2d。有興趣的同學(xué),其實(shí)還是可以想想,這些數(shù)字是怎么得到的。而我就是這樣一個(gè)好奇且好學(xué)的寶寶,我想知道他有沒有騙我,所以我查了一下資料。其中這篇文章,我覺得還是挺良心的:http://www.itdecent.cn/p/35f5f7d07732
比如就UTF-8這種編碼方式來舉個(gè)吧:
UTF-8的編碼規(guī)則很簡(jiǎn)單,只有二條:
1、對(duì)于單字節(jié)的符號(hào),字節(jié)的第一位設(shè)為0,后面7位為這個(gè)符號(hào)的unicode碼。因此對(duì)于英語字母,UTF-8編碼和ASCII碼是相同的。
2、對(duì)于n字節(jié)的符號(hào)(n>1),第一個(gè)字節(jié)的前n位都設(shè)為1,第n+1位設(shè)為0,后面字節(jié)的前兩位一律設(shè)為10。剩下的沒有提及的二進(jìn)制位,全部為這個(gè)符號(hào)的unicode碼。
看文字很費(fèi)解,上圖:
【圖略】
就拿我們的“中”字而言,它在JVM的數(shù)字是0x 4e 2d,屬于上面Unicode字符中的第三種情況,所以就可以把轉(zhuǎn)換的16個(gè)二進(jìn)制依次放入上述的x中。我利用在線的二進(jìn)制轉(zhuǎn)化武器,可以得到e4 b8 ad的結(jié)果,這就可以看出這位作者是真的很良心,糟老頭也不都是壞的。
至于其他的編碼方式,想驗(yàn)證的可以去看看規(guī)則然后動(dòng)手試一下。
上面那么多看似很高端的東西,其實(shí)看不懂也可以不用看懂,我提一下就是為了zhuangbility,因?yàn)槲覀兤綍r(shí)寫代碼完全是無感知的。了解了最多就是心里踏實(shí)一點(diǎn),不了解知道怎么用就好。但是,你要確保你真的會(huì)用,不然你的老板會(huì)不高興的。
編碼的例子代碼如下:
第一種方法,使用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;
}
實(shí)現(xiàn)的方式千千萬,但是我們一定要抓到重點(diǎn):編碼得到的什么結(jié)果。就是這玩意兒: byte[] 。對(duì),就是我們需要的字節(jié)流,就是我們需要的“物質(zhì)”。
2.3 從字節(jié)數(shù)組到數(shù)字——解碼
在完成了一系列操作以后,你還是需要讓別人知道你在想什么,最好的方式就是文字,我們大家能看得到的東西,而字節(jié)數(shù)組這東西,太過于抽象,所以我們需要把它變?yōu)橐粋€(gè)數(shù)字,這個(gè)轉(zhuǎn)化過程就是解碼。解碼就是把從磁盤或者網(wǎng)絡(luò)上得到的信息,轉(zhuǎn)換為字符或字符串。
解碼與編碼最大的區(qū)別是,解碼難。難在哪里。就是你不知道或者你沒有意識(shí)去了解,你拿到的字節(jié)之前是怎么編碼的。就好像你不知道你現(xiàn)在身邊的人之前遇到過誰。所以解碼時(shí)一定要指定字符集,否則將會(huì)使用默認(rèn)的字符集進(jìn)行解碼。如果使用了錯(cuò)誤的字符集,則會(huì)出現(xiàn)亂碼。
解碼的例子代碼如下:
第一種方法,使用String的構(gòu)造函數(shù):
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、 默認(rèn)的字符集
亂碼問題是因?yàn)槲覀冊(cè)诰幋a和解碼的過程中,采用了不一樣的字符集。有時(shí)候如果我們沒有指明編碼和解碼的方式就會(huì)采用默認(rèn)的字符集,如果我們不知道什么是默認(rèn)的字符集,就會(huì)有可能出現(xiàn)亂碼的問題。Java的默認(rèn)字符集,可以在兩個(gè)地方設(shè)定,一是執(zhí)行java程序時(shí)使用-D file.encoding參數(shù)指定,例如 -D file.encoding=UTF-8 就指定默認(rèn)字符集是UTF-8。二是在程序執(zhí)行時(shí)使用Properties進(jìn)行指定,如下:
private static void setEncoding(String charset) {
Properties properties = System.getProperties();
properties.put("file.encoding",charset);
System.out.println(properties.get("file.encoding"));
}
注意,這兩種方法如果同時(shí)使用,則程序開始時(shí)使用參數(shù)指定的字符集,在Properties方法后使用Properties指定的字符集。
如果這兩種方法都沒有使用,則使用操作系統(tǒng)默認(rèn)的字符集。例如中文版windows 7的默認(rèn)字符集是GBK。
默認(rèn)字符集的優(yōu)先級(jí)如下:
1.程序執(zhí)行時(shí)使用Properties指定的字符集;
2.java命令的-Dfile.encoding參數(shù)指定的字符集;
3.操作系統(tǒng)默認(rèn)的字符集;
4.JDK中默認(rèn)的字符集,我跟蹤了JDK1.8的源代碼,發(fā)現(xiàn)其默認(rèn)字符集指定為ISO-8859-1
4、 亂碼
從上述章節(jié)可知,字符的形態(tài)有三種,分別是“形狀”、“數(shù)字”和“字節(jié)”。字符的三種形態(tài)之間的轉(zhuǎn)換也有三類:從數(shù)字到形狀,從數(shù)字到字節(jié)(編碼),從字節(jié)到數(shù)字(解碼)。
從數(shù)字到形狀不會(huì)產(chǎn)生亂碼,亂碼就產(chǎn)生在編碼和解碼的時(shí)候。仔細(xì)想來,編碼也是不會(huì)產(chǎn)生亂碼的,因?yàn)閺臄?shù)字到字節(jié)(指定某個(gè)字符集)一定能夠轉(zhuǎn)換成功,即使某字符集中不包含該數(shù)字,它也會(huì)用指定的字節(jié)來代替,并在轉(zhuǎn)換時(shí)給出指示。
如此一來,亂碼只會(huì)產(chǎn)生在解碼時(shí):例如使用某字符集A編碼的字節(jié),使用字符集B來進(jìn)行解碼,而A和B并不兼容。這樣一來,解碼產(chǎn)生的數(shù)字(字符編碼)就是錯(cuò)誤的,那么它顯示出來也是錯(cuò)誤的。