一(yī)個故事來理解爲什麽要編碼
爲什麽會亂碼
字符集的曆史
ASCII編碼的誕生(shēng)
IOS-8859編碼家族誕生(shēng)
GB2312和GBK等雙字節編碼誕生(shēng)
Unicode字符誕生(shēng)
UTF編碼家族誕生(shēng)
UTF-32編碼
UTF-16編碼
UTF-8編碼
爲什麽有時候亂碼都是?号
拓展知(zhī)識
代碼點和代碼單元
大(dà)端模式和小(xiǎo)端模式
BOM
總結
前言
在日常開(kāi)發中(zhōng),亂碼問題可以說曾經都困擾過我(wǒ)(wǒ)們,那麽爲什麽會有亂碼發生(shēng)呢?爲什麽全世界不統一(yī)使用一(yī)套編碼呢?本文将會從字符集的發展曆史來解答這兩個問題,看完本篇,相信大(dà)家對亂碼現象會有本質上的認識。
一(yī)個故事來理解爲什麽要編碼
現在有兩個人,張三和李四,張三隻會中(zhōng)文,李四隻會英文,那麽這時候他們怎麽溝通?解決辦法是他們可以找個翻譯,這個翻譯的過程就可以理解爲編碼,也就是說從中(zhōng)文到英文或者從英文到中(zhōng)文這就是一(yī)個編碼的過程,編碼的本質就是爲了讓對方能讀懂自己的語言。
人類的各種官方語言和方言數不勝數,所以在應用到在計算機時總不能兩兩互相編碼吧?而且最重要的是人類的語言并不适合計算機使用,所以就需要發明一(yī)種适合計算機的語言,這就是二進制。二進制就是當今世界計算機的語言,當然,曾經前蘇聯也發明過三進制計算機,但是沒有普及,這個感興趣的可以自己去(qù)了解下(xià)。
有了二進制這種計算機能讀懂的語言就好辦了,當我(wǒ)(wǒ)們想和計算機溝通的時候,先轉成二進制(編碼),計算機處理完成之後,再轉換回人類語言(解碼),這就是需要編碼的原因。
爲什麽會亂碼
但是爲什麽會亂碼呢?還是用上面的故事中(zhōng)張三李四來舉例,假如有一(yī)次張三說了一(yī)個生(shēng)僻詞,然後翻譯從來沒見過這個詞,這時候翻譯就不知(zhī)道怎麽翻譯了,沒有辦法,就直接翻譯成了??,也就是亂碼了。
在計算機的世界也是同理,比如我(wǒ)(wǒ)們想從一(yī)個程序A發送雙子孤狼四個字到另一(yī)個程序B,這時候計算機數據傳輸的時候會轉成二進制,傳輸過去(qù)之後,因爲二進制不适合人類閱讀,所以B就需要進行解碼,可是現在B并不知(zhī)道A用的是什麽語言進行的編碼,所以就胡亂用英文進行解碼,解碼出來的字符英文肯定是不存在的,也就是在英文字符集裏面找不到雙子孤狼這個單詞,這時候就會發生(shēng)亂碼。
所以亂碼的本質其實就是當前編碼無法解析接收到的二進制數據。
字符集的曆史
知(zhī)道了爲什麽要編碼以及亂碼的原因之後,不禁又(yòu)有另一(yī)個疑問了,如果說全世界都統一(yī)用一(yī)種編碼,那在正常情況下(xià)也就沒有亂碼問題了,可是現實情況卻是各種編碼猶如八仙過海各顯神通,整的我(wǒ)(wǒ)們程序員(yuán)頭暈腦脹,一(yī)不留神亂碼就出來了。不過要回答這個問題那麽就需要了解一(yī)下(xià)字符集的發展曆史了。
ASCII編碼的誕生(shēng)
計算機最開(kāi)始誕生(shēng)于美國,而且計算機隻能識别二進制,所以我(wǒ)(wǒ)們就需要把常用語言和二進制關聯起來。美國人把英文裏面常用的字符以及一(yī)些控制字符轉換成了二進制數據,比如我(wǒ)(wǒ)們耳熟能詳的小(xiǎo)寫字母a,對應的十進制是97,二進制就是01100001。而一(yī)個字節有8位,即最大(dà)能表示255個字符,但是英語的常用字符比較少,常用的字母以及一(yī)些常用符号列出來就是128個,所以美國人就占用了這0-127的位置,形成了一(yī)個編碼對應關系表,這就是ASCII(AmericanStandardCodeforInformationInterchange,美國标準信息交換碼)編碼,ASCII編碼表的對應關系如果大(dà)家想知(zhī)道的可以自己去(qù)查一(yī)下(xià),這裏就不列舉了。
IOS-8859編碼家族誕生(shēng)
随着計算機的普及,計算機傳到了歐洲,這時候發現歐洲的常用字符也需要進行編碼,于是國際标準化組織(ISO)及國際電(diàn)工(gōng)委員(yuán)會(IEC)決定聯合制定另一(yī)套字符集标準。于是ISO-8859-1字符集就誕生(shēng)了。
因爲ASCII隻用到了0-127個位置,另外(wài)128-255的位置并沒有被占用(也就是一(yī)個字節的最高位并沒有被使用),于是歐洲人就把第8位利用了起來,從此這128-255就被西歐常用字符占用了,ISO-8859-1字符也叫做Latin1編碼。
慢(màn)慢(màn)的,随着時間的推移,歐洲越來越多國家的字符需要編碼,所以就衍生(shēng)了一(yī)系列的字符集,從ISO-8859-1到ISO-8859-16經過了一(yī)系列的微調,但是這些都屬于ISO-8859标準。
需要注意的是,ISO-8859标準是向下(xià)兼容ASCII字符集的,所以平常我(wǒ)(wǒ)們見到的許多場景下(xià)默認都是用的ISO-8859-1編碼比較多,而不會直接使用ASCII編碼。
GB2312和GBK等雙字節編碼誕生(shēng)
慢(màn)慢(màn)的,随着時間的推移,計算機傳到了亞洲,傳到了中(zhōng)國以及其他國家,這時候許多國家都針對自己國家的常用文字制定了自己國家的編碼,中(zhōng)國也不例外(wài)。
但是這個時候卻發現,一(yī)個字節的8位已經全部被占用了,于是隻能再擴展一(yī)個字節,也就是用2個字節來存儲。但是兩個字節來存儲又(yòu)有一(yī)個問題,那就是比如我(wǒ)(wǒ)讀取了兩個字節出來,這兩個字節到底是表示兩個單字節字符還是表示的是雙字節的中(zhōng)文呢?
于是我(wǒ)(wǒ)們偉大(dà)的中(zhōng)國人民就決定制定一(yī)套中(zhōng)文編碼,用來兼容ASCII,因爲ASCII編碼中(zhōng)的單字節字符一(yī)定是小(xiǎo)于128的,所以最後我(wǒ)(wǒ)們就決定,中(zhōng)文的雙字節字符都從128之後開(kāi)始,也就是當發現字符連續兩位都大(dà)于128時,就說明這是一(yī)個中(zhōng)文,指定了之後我(wǒ)(wǒ)們就把這種編碼方式稱之爲GB2312編碼。
需要注意的是GB2312并不兼容ISO-8859-n編碼集,但是兼容ASCII編碼。
GB2312編碼收錄了常用的漢字6763個和非漢字圖形字符682(包括拉丁字母、希臘字母、日文平假名及片假名字母、俄語西裏爾字母在内的全角字符)個。
随着計算機的更進一(yī)步普及,GB2312也暴露出了問題,那就是GB2312中(zhōng)收錄的中(zhōng)文漢字都是簡體(tǐ)字和常用字,對于一(yī)些生(shēng)僻字以及繁體(tǐ)字沒有收錄,于是乎GBK出現了。
GB2312編碼因爲兩個字節采用的都是高位,就算全部對應上,最大(dà)也隻能存儲16384個漢字,而我(wǒ)(wǒ)國漢字如果加上繁體(tǐ)字和生(shēng)僻字是遠遠不夠的,于是GBK的做法就是隻要求第一(yī)位是大(dà)于128,第二位可以小(xiǎo)于128,這就是說隻要發現一(yī)個字節大(dà)于128,那麽緊随其後的一(yī)個字節就是和其作爲一(yī)個整體(tǐ)作爲中(zhōng)文字符,這樣最多就能存儲32640個漢字了。當然,GBK并沒有全部用完,GBK共收入21886個漢字和圖形符号,其中(zhōng)漢字(包括部首和構件)21003個,圖形符号883個。
後面随着計算機的再進一(yī)步普及,我(wǒ)(wǒ)們也慢(màn)慢(màn)擴展了其他的中(zhōng)文字符集,比如GB18030等,但是這些都屬于雙字節字符。
到這裏希望大(dà)家明白(bái),爲什麽英文是一(yī)個字符,中(zhōng)文是兩個甚至更多字符了。一(yī)個原因就是低位被用了,另一(yī)個就是常用中(zhōng)文字符太多了,一(yī)個字節是遠遠存不完的。
Unicode字符誕生(shēng)
其實計算機在發展過程中(zhōng),不單單是美國,歐洲和中(zhōng)國,其他許多國家都有自己的字符,比如日本,韓國等都有自己的字符集,可以說很混亂,于是有關部門看不下(xià)去(qù)了,決定結束這種世界大(dà)戰的混亂局面,重新制定另一(yī)套字符标準,這就是Unicode。
從一(yī)出生(shēng)開(kāi)始,Unicode就覺得除了自己,其他各位都是渣渣。所以它壓根就沒準備兼容其他編碼,直接另起爐竈來了一(yī)套标準。Unicode字符最開(kāi)始采用的是UCS-2标準,UCS-2标準規定一(yī)個字符至少使用2個字節來表示。當然,2個字節即使全被利用也隻能存儲65536個字符,這肯定容納不了世界上所有的語言和符号以及控制字符,所以後面又(yòu)有了UCS-4标準,可以用4個字節來存儲一(yī)個字符,四個字節來存儲全世界所有語言文字和控制字符是基本沒有問題了。
需要注意的是:Unicode編碼隻是定義了字符集,對于字符集具體(tǐ)應該如何存儲并沒有做要求。站在我(wǒ)(wǒ)們開(kāi)發的角度,相當于Unicode隻定義了接口,但是沒有具體(tǐ)的實現。
UTF編碼家族誕生(shēng)
UTF系列編碼就是對Unicode字符集的實現,隻不過實現的方式有所區别,其中(zhōng)主要有:UTF-8,UTF-16,UTF-32等類型。
UTF-32編碼
UTF-32編碼基本按照Unicode字符集标準來實現,任何一(yī)個符号都占用4個字節。可以想象,這會浪費(fèi)多大(dà)空間,對英文而言,空間擴大(dà)了四倍,中(zhōng)文也擴大(dà)了兩倍,所以這種編碼方式也導緻了Unicode在最初并沒有被大(dà)家廣泛的接受。
UTF-16編碼
UTF-16編碼相比較UTF-32做了一(yī)點改進,其采用2個字節或者4個字節來存儲。大(dà)部分(fēn)情況下(xià)UTF-16編碼都是采用2個字節來存儲,而當2個字節存儲時,UTF-16編碼會将Unicode字符直接轉成二進制進行存儲,對于另外(wài)一(yī)些生(shēng)僻字或者使用較少的符号,UTF-16編碼會采用4個字節來存儲,但是采用四個字節存儲時需要做一(yī)次編碼轉換。
下(xià)表就是UTF-16編碼的存儲格式:
Unicode編碼範圍(16進制)
UTF-16編碼的二進制存儲格式
0x00000000-0x0000FFFF
xxxxxxxxxxxxxxxx
0x00010000-0x0010FFFF
110110xxxxxxxxxx110111xxxxxxxxxx
這個表先不解釋,後面解釋UTF-8編碼時會一(yī)起說明。
UTF-8編碼
UTF-8是一(yī)種變長的編碼,兼容了ASCII編碼,爲了實現變長這個特性,那麽就必須要有一(yī)個規範來規定存儲格式,這樣當程序讀了2個或者多個字節時能解析出這到底是表示多個單字節字符還是一(yī)個多字節字符。
UTF-8編碼的存儲規範如下(xià)表所示:
Unicode編碼範圍(16進制)
UTF-8編碼的二進制存儲格式
0x00000000-0x0000007F
0xxxxxxx
0x00000080-0x000007FF
110xxxxx10xxxxxx
0x00000800-0x0000FFFF
1110xxxx10xxxxxx10xxxxxx
0x00010000-0x0010FFFF
11110xxx10xxxxxx10xxxxxx10xxxxxx
接下(xià)來我(wǒ)(wǒ)們以雙字爲例來進行說明:
雙:對應的Unicode編碼爲\u53cc,轉成二進制就是:101001111001100,這時候表格中(zhōng)的第一(yī)行隻有7位存不下(xià)去(qù),第二列也隻有11位,也不夠存儲,所以隻能存儲到第三列,第三列有16位,從後往前依次填補x的位置,填完之後還有一(yī)位空餘,直接補0,最終得到:111001011000111110001100,所以雙字就占用了3個字節,當然,有些生(shēng)僻字會占用到四個字節。
所以上面的UTF-16編碼也是同理,如果當前字符采用的是兩字節存儲,那麽直接轉成二進制存儲即可,位數不足直接補0即可,而當采用4個字節存儲時,則需要和UTF-8一(yī)樣進行一(yī)次轉換,也就是說隻能将其填充到x的位置,x之外(wài)的是固定格式。
需要注意的是:在UTF-16編碼中(zhōng),2個字節也可能出現4字節中(zhōng)110110xx或者110111xx開(kāi)頭的格式,這兩部分(fēn)對應的區間分(fēn)别是:D800~DBFF和DC00~DFFF,所以爲了避免這種歧義的發生(shēng),這兩部分(fēn)區間是是專門空出來的,沒有進行編碼。
爲什麽有時候亂碼都是?号
在Java開(kāi)發中(zhōng),經常會碰到亂碼顯示爲?号,比如下(xià)面這個例子:
Stringname="雙子孤狼";byte[]bytes=name.getBytes(StandardCharsets.ISO_8859_1);System.out.println(newString(bytes));//輸出:????
這個輸出結果的原因是中(zhōng)文無法用ISO_8859_1編碼進行存儲,而示例中(zhōng)卻強制用ISO_8859_1編碼進行解碼。
在Java中(zhōng)提供了一(yī)個ISO_8859_1類用來解碼,解碼時當發現當前字符轉成十進制之後大(dà)于255時就會直接不進行解碼,轉而直接賦一(yī)個默認值63,所以上面的示例中(zhōng)的byte數組結果就是63636363,而63在ASCII中(zhōng)就恰好就對應了?号。
所以一(yī)般我(wǒ)(wǒ)們看到編碼出現?基本就說明當前是采用ISO_8859_1進行的解碼,而當前的字符又(yòu)大(dà)于255。
拓展知(zhī)識
了解了編碼發展曆史之後,接下(xià)來就讓我(wǒ)(wǒ)們一(yī)起了解下(xià)其他和編碼相關的題外(wài)話(huà)。
代碼點和代碼單元
在Java中(zhōng)的字符串是由char序列組成,而char又(yòu)是采用UTF-16表示的Unicode代碼點的代碼單元。這句話(huà)裏面涉及到了代碼點和代碼單元,初次接觸的朋友可能會有點迷惑,但是了解了Unicode字符集标準和UTF-16的編碼方式之後就比較好理解。
代碼點:一(yī)個代碼點等同于一(yī)個Unicode字符。
代碼單元:在UTF-16中(zhōng),兩個字節表示一(yī)個代碼單元,代碼單元是最小(xiǎo)的不可拆分(fēn)的部分(fēn),所以如果在UTF-8中(zhōng),一(yī)個代碼單元就是一(yī)個字節,因爲UTF-8中(zhōng)可以用一(yī)個字節表示一(yī)個字符。
平常我(wǒ)(wǒ)們調用字符串的length方法,返回的就是代碼單元數量,而不是代碼點數量,所有如果碰到一(yī)些需要用4個字節來表示的繁體(tǐ)字,那麽代碼單元數就會小(xiǎo)于代碼點數,而想要獲取代碼點數量,可以通過其他方法獲取,獲取方式如下(xià):
Stringname="䭢";//\uD852\uDF62System.out.println(name.length);//代碼單元數,輸出2System.out.println(name.codePointCount(0,name.length));//代碼點數,輸出1
大(dà)端模式和小(xiǎo)端模式
在計算機中(zhōng),數據的存儲是以字節爲單位的,那麽當一(yī)個字符需要使用多個字節來表示的時候,就會産生(shēng)一(yī)個問題,那就是多字節字符應該從前往後組合還是從後往前組合。
還是以雙字爲例,轉成二進制爲:0101001111001100,以一(yī)個字節爲單位,就可以拆分(fēn)成:01010011和11001100,其中(zhōng)第一(yī)部分(fēn)就稱之爲高位字節,第二部分(fēn)就稱之爲低位字節,将這兩部分(fēn)順序互換存儲就産生(shēng)了大(dà)端模式和小(xiǎo)端模式。
大(dà)端模式(Big-endian):顧名思義就是以高位字節結尾,低位在前(左),高位在後(右)。如雙字就會存儲爲:1100110001010011。
小(xiǎo)端模式(Little-endian):顧名思義就是以低位字節結尾,高位在前(左),低位在後(右)。如雙字就會存儲爲:0101001111001100(和我(wǒ)(wǒ)們平常計算二進制的邏輯一(yī)緻,從右到左依次從2的0次方開(kāi)始)。
注:在Java中(zhōng)默認采用的是大(dà)端模式,雖然底層的處理器可能會采用不同的模式存儲字節,但是因爲有JVM的存在,這些細節已經被屏蔽,所以平常大(dà)家可能也沒有很關注這些。
BOM
既然底層存儲分(fēn)爲了大(dà)端和小(xiǎo)端兩種模式,那麽假如我(wǒ)(wǒ)們現在有一(yī)個文件,計算機又(yòu)是怎麽知(zhī)道當前是采用的大(dà)端模式還是小(xiǎo)端模式呢?
BOM即byteordermark(字節順序标記),出現在文本文件頭部。BOM就是用來标記當前文件采用的是大(dà)端模式還是小(xiǎo)端模式存儲。我(wǒ)(wǒ)想這個大(dà)家應該都見過,平常在使用記事本保存文檔的時候,需要選擇采用的是大(dà)端還是小(xiǎo)端:
在UCS編碼中(zhōng)有一(yī)個叫做ZeroWidthNo-BreakSpace(零寬無間斷間隔)的字符,對應的編碼是FEFF。FEFF是不存在的字符,正常來說不應該出現在實際數據傳輸中(zhōng)。
但是爲了區分(fēn)大(dà)端模式和小(xiǎo)端模式,UCS規範建議在傳輸字節流前,先傳輸字符ZeroWidthNo-BreakSpace。而且根據這個字符的順序來區分(fēn)大(dà)端模式和小(xiǎo)端模式。
下(xià)表就是不同編碼的BOM:
編碼
16進制BOM
UTF-8
EFBBBF
UTF-16(大(dà)端模式)
FEFF
UTF-16(小(xiǎo)端模式)
FFFE
UTF-32(大(dà)端模式)
0000FEFF
UTF-32(小(xiǎo)端模式)
FFFE0000
有了這個規範,解析文件的時候就可以知(zhī)道當前編碼以及其存儲模式了。注意這裏UTF-8編碼比較特殊,因爲本身UTF-8編碼有特殊的順序格式規定,所以UTF-8本身并沒有什麽大(dà)端模式和小(xiǎo)端模式的區别.
根據UTF-8本身的特殊編碼格式,在沒有BOM的情況下(xià)也能被推斷出來,但是因爲微軟是建議都加上BOM,所以目前存在了帶BOM的UTF-8文件和不帶BOM的UTF-8文件,這兩種格式在某些場景可能會出現不兼容的問題,所以在平常使用中(zhōng)也可以稍微留意這個問題。
總結
本文主要從編碼的曆史開(kāi)始,講述了編碼的存儲規則并且分(fēn)析了産生(shēng)亂碼的本質原因,同時也分(fēn)析了字節的兩種存儲模型以及BOM相關問題,通過本文相信對于項目中(zhōng)出現的亂碼問題,大(dà)家會有一(yī)個清晰的思路來分(fēn)析問題。