寫在前面
編解碼的問題是個(gè)蠻大的概念迷雪,網(wǎng)上也搜過一些資料,講的也都蠻詳細(xì)的僵刮,寫此一篇也是給自己做個(gè)紀(jì)要递礼,以免日后再遇見編解碼問題無法解決
遇見問題
在項(xiàng)目中使用的dubbo來做服務(wù)間通信的方式涂臣,有一個(gè)需求是將一個(gè)圖片流顯示給前端盾计,由于項(xiàng)目依賴層級(jí)的原因售担,這個(gè)圖片流只能在服務(wù)的最底層生成,然后向上傳遞署辉,最后交給調(diào)用者族铆。一開始就簡(jiǎn)單的將輸出流inputStream作為返回值傳出去了,結(jié)果報(bào)錯(cuò),大意是不識(shí)別某個(gè)字符哭尝,剛開始沒想到是編碼格式的問題哥攘,以為就是不能傳數(shù)據(jù)流,就想著要不把流轉(zhuǎn)成string傳出去材鹦,結(jié)果試了之后dubbo倒是沒報(bào)錯(cuò)逝淹,生成的圖片無法打開,debug發(fā)現(xiàn)桶唐,底層生成的inputStream的數(shù)據(jù)長(zhǎng)度跟最后string轉(zhuǎn)成的inputStream的長(zhǎng)度不相同栅葡,才想到可能是編碼格式的問題。
尋求解答
其實(shí)想到是編碼格式的問題莽红,第一反應(yīng)是設(shè)置一個(gè)編碼格式好了妥畏,于是給轉(zhuǎn)碼和解碼都加了UTF-8格式的要求邦邦,結(jié)果一看仍然是不對(duì)的安吁,這就有點(diǎn)懵逼了,因?yàn)楸旧韺?duì)這塊不是很熟燃辖,就想著那要不就稍微研究一下是為什么好了鬼店。
首先是原理
從原理上來說,計(jì)算機(jī)都是以二進(jìn)制來儲(chǔ)存所有的數(shù)據(jù)的黔龟,那要表達(dá)全世界各種各樣的字符妇智,就需要有相應(yīng)的編碼格式,告訴計(jì)算機(jī)要以什么樣的規(guī)則來解析一段指定的二進(jìn)制數(shù)據(jù)氏身。而計(jì)算機(jī)在讀取二進(jìn)制數(shù)據(jù)的時(shí)候又規(guī)定了以八個(gè)二進(jìn)制位為一個(gè)字節(jié)巍棱,即1byte,由于每位二進(jìn)制可以有0和1兩種形式蛋欣,所以1byte可以表示256種不同含義航徙。那用不同的編碼格式來讀取同樣的二進(jìn)制,可以得到不同的結(jié)果陷虎,比如同樣的1000100011110001到踏,這是十六位二進(jìn)制數(shù)據(jù),以ISO-8859-1解析和以UTF-8來解析尚猿,結(jié)果就是完全不一樣的意思(當(dāng)然我不知道這具體是啥意思窝稿,隨手寫的,保證結(jié)果不一樣而已)凿掂。這也就是為什么伴榔,同樣的一份txt文件,可以是一堆亂碼,也可以是正常顯示的文字的原因踪少。
然后是原因
上面講了一些計(jì)算機(jī)讀取文件時(shí)顯示具體內(nèi)容的原理骗灶,提到了編碼格式,現(xiàn)在為人熟知且常用的就那幾種秉馏,ASCII耙旦,ISO-8859-1,UTF-8,UTF-16,GBK萝究。這里面有兩組是很相似的免都,一個(gè)是ASCII和ISO-8859-1,一個(gè)是UTF-8和UTF-16帆竹。首先是ASCII和ISO-8859-1绕娘,同樣都是采用單字節(jié)讀取,即用一個(gè)字節(jié)表示一個(gè)字母栽连,不同的是ASCII表只能表示128個(gè)字符险领,因?yàn)樽罡呶皇?不變的;而ISO-8859-1會(huì)用到最高位的值秒紧,所以能夠表示256種不同的字符绢陌。而UTF-8和UTF-16則是Unicode編碼的實(shí)現(xiàn),這里就不得不講一下Unicode編碼和UTF-8熔恢,UTF-16的區(qū)別脐湾。后兩者的區(qū)別比較簡(jiǎn)單,UTF-8采用的是1~4個(gè)字節(jié)不定長(zhǎng)的存儲(chǔ)格式叙淌,而UTF-16則是兩個(gè)字節(jié)或四個(gè)字節(jié)來存儲(chǔ)字符秤掌。Unicode是我們熟知的編碼字符集,而其實(shí)也只是字符集鹰霍,只規(guī)定了符號(hào)的二級(jí)制代碼闻鉴,并沒有規(guī)定如何存儲(chǔ)。什么意思茂洒,比如一個(gè)“漢”字孟岛,Unicode編碼為6C49,轉(zhuǎn)化成二進(jìn)制是1101100 01001001获黔,這并不意味著計(jì)算機(jī)存儲(chǔ)這個(gè)“漢”字就是這些二進(jìn)制蚀苛,還取決于具體的編碼格式。所以通俗點(diǎn)說可以將Unicode理解為接口玷氏,而UTF-8和UTF-16理解為是接口的具體實(shí)現(xiàn)堵未。
而上面這個(gè)“漢”字是如何存儲(chǔ)的呢,比如我們采用UTF-8的方式來存儲(chǔ)盏触,UTF-8定義了兩個(gè)簡(jiǎn)單的編碼規(guī)則:
- 對(duì)于單字節(jié)的符號(hào)渗蟹,字節(jié)第一位設(shè)0块饺,后面的是這個(gè)符號(hào)的Unicode編碼。所以對(duì)于英文字母雌芽,UTF-8編碼和ASCII碼是一樣的授艰。
- 對(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碼谷朝。
Unicode符號(hào)范圍 | UTF-8編碼方式
(十六進(jìn)制) | (二進(jìn)制)
----------------------+---------------------------------------------
0000 0000-0000 007F | 0xxxxxxx
0000 0080-0000 07FF | 110xxxxx 10xxxxxx
0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx
0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
上面是具體的Unicode轉(zhuǎn)UTF-8的編碼規(guī)則,根據(jù)這張表武花,可以很輕松的定位不同的Unicode編碼對(duì)應(yīng)的UTF-8的編碼格式圆凰,由于“漢”的Unicode編碼是6C49,落在了上面的第三個(gè)區(qū)間,所以UTF-8編碼格式則是111001101011000110001001,即用三個(gè)字節(jié)來儲(chǔ)存体箕。
接著是實(shí)踐
了解了上面的理論专钉,就隨手實(shí)踐一下,看看結(jié)果累铅。首先是想一下情景跃须,上面問題中提到的是從流轉(zhuǎn)成字符,然后從字符再轉(zhuǎn)成流争群,所以模擬一下這個(gè)過程
byte[] bytes = new byte[2];
bytes[0] = (byte) 130;
bytes[1] = 88;
String test = new String(bytes);
System.out.print(test);
上面這段代碼模式的是字節(jié)流轉(zhuǎn)成字符流的過程回怜,注意byte[0]需要強(qiáng)轉(zhuǎn)而1不需要大年,是因?yàn)镴ava默認(rèn)一個(gè)byte的大小是0~127换薄,最高位是符號(hào)位,而130由于大于127翔试,則按照規(guī)定轻要,是取互補(bǔ)的那個(gè)負(fù)數(shù),即-126(如果說設(shè)置的是大于256垦缅,則一個(gè)字節(jié)無法表示冲泥,需要多個(gè)字節(jié),這里面就需要更多的運(yùn)算了壁涎,Java則是取最低的那8位)凡恍。然后new一個(gè)string出來,這里沒有設(shè)置編碼格式怔球,Java默認(rèn)使用的是UTF-8(可以通過System.out.println(Charset.defaultCharset());
來打印默認(rèn)的編碼格式)嚼酝,打印出來的結(jié)果是?X
,可以看見有兩個(gè)字符,第一個(gè)是亂碼竟坛,第二個(gè)識(shí)別出來是X闽巩,亂碼是因?yàn)檫@個(gè)二進(jìn)制流按照UTF-8的對(duì)照表找不到對(duì)應(yīng)的字符钧舌,所以就無法顯示。
這個(gè)時(shí)候再將這個(gè)字符流轉(zhuǎn)成字節(jié)流試試呢
String test = new String(bytes);
byte[] bytes1 = test.getBytes();
debug一下看看結(jié)果涎跨,會(huì)發(fā)現(xiàn)byte1的數(shù)組長(zhǎng)度不再是2了洼冻,而是4位
可以發(fā)現(xiàn)最后一位仍然是88,但是前三位是由130代表的字符流轉(zhuǎn)變而來的隅很,原因則也是因?yàn)?30代表的Unicode字符大小落在了上面總結(jié)的編碼表的第三區(qū)間撞牢,所以需要三個(gè)字節(jié)來存儲(chǔ),于是乎變成了三位叔营,感興趣的可以將這三個(gè)字節(jié)轉(zhuǎn)成二進(jìn)制看看是什么普泡。
最后是結(jié)論
有了上面的一個(gè)小實(shí)踐,這里明白了兩個(gè)問題
- 我在最開始那個(gè)流轉(zhuǎn)成字符串审编,再由字符串轉(zhuǎn)成流的問題中遇見了一種情況撼班,我使用ASCII碼編碼的時(shí)候,圖片無法顯示出來垒酬,但是字節(jié)數(shù)轉(zhuǎn)化前后是一樣的砰嘁;但是當(dāng)我使用ISO_8859_1編碼格式的時(shí)候,就可以顯示出圖片了勘究。于是可以得到的結(jié)論是這個(gè)圖片流里面的字符肯定都是那0-256范圍中的字符矮湘,由于ASCII碼表無法表示128-256,所以才會(huì)有字節(jié)數(shù)一樣口糕,但是結(jié)果不一樣的情況缅阳。
- 當(dāng)我用UTF-8轉(zhuǎn)變的時(shí)候,發(fā)現(xiàn)轉(zhuǎn)換后的字節(jié)數(shù)組會(huì)比一開始的多很多景描,這可能是因?yàn)閳D片流中存在UTF-8無法識(shí)別的字符十办,比如,舉個(gè)極端的例子超棺,所有的8位二進(jìn)制全部是10xxxxxx形式的向族,對(duì)應(yīng)上面的UTF-8與Unicode轉(zhuǎn)碼表,就會(huì)發(fā)現(xiàn)無法對(duì)應(yīng)棠绘,自然是得不到想要的結(jié)果的
再次象征性總結(jié)
這次碼的也挺亂的件相,其實(shí)對(duì)Java中字節(jié)流與字符流的了解確實(shí)不夠透徹,也源于對(duì)計(jì)算機(jī)原理的基礎(chǔ)了解還不足氧苍,這次也算是個(gè)引子夜矗,后面還是要再多研究強(qiáng)化一下這方面的知識(shí)。
還有让虐,這里參考了很多阮一峰大神的一篇關(guān)于編解碼的文章紊撕,大神就是大神,講的就是通俗易懂澄干,雖然是十幾年前的文章逛揩,但是仍然有很強(qiáng)的可讀性
長(zhǎng)路漫漫~~