作為一名程序員,肯定有被亂碼困擾的時(shí)候,真到了百思不得其解的時(shí)候,就會(huì)覺(jué)得:英文程序員真幸福。 但其實(shí)只要明白編碼之間的轉(zhuǎn)換規(guī)律,其實(shí)亂碼so easy~
我們知道,計(jì)算機(jī)存儲(chǔ)數(shù)據(jù)都是2進(jìn)制,就是0和1,那么這么多的字符就都需要有自己對(duì)應(yīng)的0和1組成的序列,計(jì)算機(jī)將需要存儲(chǔ)的字符轉(zhuǎn)換成它們對(duì)應(yīng)的01序列,然后就可以儲(chǔ)存在電腦里了。
比如我們可以定義用8位2進(jìn)制表示一個(gè)字符,“00000000”表示小寫(xiě)字母“a”,“00000001”表示小寫(xiě)字母“b”,那么計(jì)算機(jī)要存儲(chǔ)“ab”的時(shí)候,其實(shí)在計(jì)算機(jī)里的存儲(chǔ)的是“0000000000000001”,讀取的時(shí)候先讀取前8位,根據(jù)對(duì)應(yīng)關(guān)系,可以解碼出“a”,再讀取后8位,又可以解碼出“b”,這樣就讀出了當(dāng)時(shí)寫(xiě)入的“ab”了。而我們定義的這種字符和二進(jìn)制序列的對(duì)應(yīng)關(guān)系,就可以稱之為編碼。我們?nèi)绻枰獙ⅰ癮b”發(fā)送給別人,因?yàn)榫W(wǎng)絡(luò)也是基于二進(jìn)制,所以只要先約定好編碼規(guī)則,就可以發(fā)送“0000000000000001”,然后對(duì)方根據(jù)約定的編碼解碼,就可以得到“ab”?,F(xiàn)在是互聯(lián)網(wǎng)的時(shí)代,我們經(jīng)常需要和其他的計(jì)算機(jī)進(jìn)行交互,一套編碼系統(tǒng)還是比較復(fù)雜的,所以大家就需要約定統(tǒng)一的編碼,這樣的編碼是大家都約定好的,就不用再去約定編碼規(guī)則了~然而,為了滿足各種不同的需求,人們還是制定了很多種編碼,沒(méi)有哪一種能全面替代其他編碼,所以現(xiàn)在多種編碼并存。通常這些編碼都被大家所接受和熟知,所以現(xiàn)在不用再通信前商量編碼的對(duì)應(yīng)規(guī)則和細(xì)節(jié),只需要告訴對(duì)方,我采用的是什么通用編碼,彼此就能愉快地通信了。
所以亂碼的本質(zhì)就是:讀取二進(jìn)制的時(shí)候采用的編碼和最初將字符轉(zhuǎn)換成二進(jìn)制時(shí)的編碼不一致。
ps:編碼有動(dòng)詞含義也有名詞含義,名詞含義就是一套字符和二進(jìn)制序列之間的轉(zhuǎn)換規(guī)則,動(dòng)詞含義是使用這種規(guī)則將字符轉(zhuǎn)換成二進(jìn)制序列。
好了,廢話不多,直接上一段代碼:
因?yàn)閁TF-8和GBK是兩套中文支持較好的編碼,所以經(jīng)常會(huì)進(jìn)行它們之間的轉(zhuǎn)換,這里就以它們舉例。 以上代碼運(yùn)行打印出以下內(nèi)容:
UTF-8轉(zhuǎn)換成GBK:鎴戜滑鏄腑鍥戒漢
我們看到,將"我們是中國(guó)人"以UTF-8編碼轉(zhuǎn)換成byte數(shù)組(byte數(shù)組其實(shí)就相當(dāng)于二進(jìn)制序列了,此過(guò)程即編碼),再以GBK編碼和byte數(shù)組創(chuàng)建新的字符串(此過(guò)程即以GBK編碼去解碼byte數(shù)組,得到字符串),就產(chǎn)生亂碼了。 因?yàn)?strong>編碼采用的UTF-8和解碼采用的GBK不是同一種編碼,所以最后結(jié)果亂碼了。 之后再對(duì)亂碼使用GBK編碼,還原到解碼前的byte數(shù)組,再使用和最初編碼時(shí)使用的一致的編碼UTF-8進(jìn)行解碼,就可得到最初的“我們是中國(guó)人”。 這種多余的轉(zhuǎn)換有時(shí)候還是很有用的,比如ftp協(xié)議只支持ISO-8859-1編碼,這個(gè)時(shí)候如果要傳中文,只能先換成ISO-8859-1的亂碼,ftp完成后,再轉(zhuǎn)回UTF-8就又可以得到正常的中文了。
怎么樣?編碼轉(zhuǎn)換是不是so easy?那該來(lái)點(diǎn)正經(jīng)的了: 這次我們反過(guò)來(lái),先將字符串以GBK編碼再以UTF-8解碼,再以UTF-8編碼,再以GBK解碼。
這次的運(yùn)行結(jié)果是:
GBK轉(zhuǎn)換成UTF-8:???????й???
WTF??萬(wàn)惡的“錕斤拷”,相信不少人都見(jiàn)過(guò)。這里GBK轉(zhuǎn)成UTF-8亂碼好理解,但是再轉(zhuǎn)回來(lái)怎么變成了“錕斤拷錕斤拷錕斤拷錕叫癸拷錕斤拷”,這似乎不科學(xué)。 這其實(shí)和UTF-8獨(dú)特的編碼方式有關(guān),由于UTF-8需要對(duì)unicode字符進(jìn)行編碼,unicode字符集是一個(gè)幾乎支持所有字符的字符集,為了表示這么龐大的字符集,UTF-8可能需要更多的二進(jìn)制位來(lái)表示一個(gè)字符,同時(shí)為了不致使UTF-8編碼太占存儲(chǔ)空間,根據(jù)二八定律,UTF-8采用了一種可變長(zhǎng)的編碼方式,即將常用的字符編碼成較短的序列,而不常用的字符用較長(zhǎng)的序列表示,這樣讓編碼占用更少存儲(chǔ)空間的同時(shí)也保證了對(duì)龐大字符集的支持。 正式由于UTF-8采用的這種特別的變長(zhǎng)編碼方式,這一點(diǎn)和其他的編碼很不一樣。比如GBK固定用兩個(gè)字節(jié)來(lái)表示漢字,一個(gè)字節(jié)來(lái)表示英文和其他符號(hào)。
來(lái)測(cè)試一下:
GbkBytes.length:12
可以看到使用GBK進(jìn)行編碼,“我們是中國(guó)人”6個(gè)漢字占12個(gè)字節(jié),而是用UTF-8進(jìn)行編碼則占了18個(gè)字節(jié),其中每個(gè)漢字占3個(gè)字節(jié)(由于是常用漢字,只占3個(gè)字節(jié),有的稀有漢字會(huì)占四個(gè)字節(jié)。) UTF-8編碼的讀取方式也比較不同,需要先讀取第一個(gè)字節(jié),然后根據(jù)這個(gè)字節(jié)的值才能判斷這個(gè)字節(jié)之后還有幾個(gè)字節(jié)共同參與一個(gè)字符的表示。 對(duì)于某一個(gè)字符的UTF-8編碼,如果只有一個(gè)字節(jié)則其最高二進(jìn)制位為0;如果是多字節(jié),其第一個(gè)字節(jié)從最高位開(kāi)始,連續(xù)的二進(jìn)制位值為1的個(gè)數(shù)決定了其編碼的位數(shù),其余各字節(jié)均以10開(kāi)頭。UTF-8最多可用到6個(gè)字節(jié)。
如表: 上面一隨便看看就好,只要知道“由于UTF-8的特殊編碼方式,所以有些序列是不可能出現(xiàn)在UTF-8編碼中的”就可以了。
所以當(dāng)我們將由GBK編碼的12個(gè)字節(jié)試圖用UTF-8解碼時(shí)會(huì)出現(xiàn)錯(cuò)誤,由于GBK編碼出了不可能出現(xiàn)在UTF-8編碼中出現(xiàn)的序列,所以當(dāng)我們?cè)噲D用UTF-8去解碼時(shí),經(jīng)常會(huì)遇到這種不可能序列,對(duì)于這種不可能序列,UTF-8把它們轉(zhuǎn)換成某種不可言喻的字符“?”,當(dāng)這種不可言喻的字符再次以UTF-8進(jìn)行編碼時(shí),他們已經(jīng)無(wú)法回到最初的樣子了,因?yàn)槟切┦荱TF-8編碼不可能編出的序列。然后這個(gè)神秘字符再轉(zhuǎn)換成GBK編碼時(shí)就變成了“錕斤拷”。當(dāng)然,還有很多其他的巧合,可能正好碰到UTF-8中存在的序列,甚至原本不是一個(gè)字符的字節(jié),可能是某個(gè)字的第二個(gè)字節(jié)和下一個(gè)字的兩個(gè)字節(jié),正好被識(shí)別成一個(gè)UTF-8序列,于是解碼出一個(gè)漢字,當(dāng)然這些在我們看來(lái)都是亂碼了,只不過(guò)不是“錕斤拷”的樣子。因?yàn)椴豢赡苄蛄懈毡榇嬖?,所以GBK轉(zhuǎn)UTF-8再轉(zhuǎn)GBK時(shí),最常見(jiàn)的便是“錕斤拷”!
所以:以非UTF-8編碼編碼出的字節(jié)數(shù)組,一旦以UTF-8進(jìn)行解碼,通常這是一條不歸路,再嘗試將解碼出的字符以UTF-8進(jìn)行編碼,也無(wú)法還原之前的字節(jié)數(shù)組。 相反地,其他的固定長(zhǎng)度編碼幾乎都可以順利還原。
=====================2016/11/15補(bǔ)充========================== 上文中其實(shí)有一個(gè)東西一直在回避,就是既然所有字符在保存時(shí)都需要轉(zhuǎn)換成二進(jìn)制,那么java是使用什么編碼來(lái)保存字符的呢?這個(gè)問(wèn)題其實(shí)我們可以不必深究,因?yàn)檫@對(duì)我們是透明的,我們只要假設(shè)java使用某種編碼可以表示所有字符。得益于這種透明,我們可以當(dāng)作java是直接保存字符本身的,就如上文所做的這樣。但是今天面試的時(shí)候被問(wèn)到了,我說(shuō)這個(gè)是對(duì)我們透明所以沒(méi)有深究。他說(shuō)雖然是透明的,但是如果弄懂其中的原理還是能加深理解。我馬上想到unicode,因?yàn)閖ava要準(zhǔn)確地表示所有字符,那么只有unicode能勝任了。這個(gè)回答也得到面試官的肯定,還說(shuō)了一些更細(xì)節(jié)的。每種編碼都會(huì)提供和unicode編碼之間的轉(zhuǎn)換規(guī)則。當(dāng)我們以字符串直接量new一個(gè)String,這個(gè)String就是以u(píng)nicode在內(nèi)存中存儲(chǔ)的。同樣這也解決了一個(gè)讓我疑惑的問(wèn)題:為什么一個(gè)char中既可以存儲(chǔ)一個(gè)字母,也可以存儲(chǔ)一個(gè)漢字,明明很多編碼如GBK、UTF-8中漢字和字母的長(zhǎng)度不一樣。如果java虛擬機(jī)使用unicode編碼,那這一切就很好理解了,字母和漢字長(zhǎng)度一樣。
新增一條結(jié)論:java虛擬機(jī)中以使用unicode編碼保存字符,任何編碼都提供了和unicode編碼的轉(zhuǎn)換規(guī)則。
|
|
來(lái)自: liang1234_ > 《關(guān)于編碼》