您好,登錄后才能下訂單哦!
這篇文章主要介紹了Java中ANSI,Unicode,BMP,UTF等編碼概念的示例分析,具有一定借鑒價(jià)值,感興趣的朋友可以參考下,希望大家閱讀完這篇文章之后大有收獲,下面讓小編帶著大家一起了解一下。
一、前言
其實(shí)從開(kāi)始寫Java代碼以來(lái),我遇到過(guò)無(wú)數(shù)次亂碼與轉(zhuǎn)碼問(wèn)題,比如從文本文件讀入到String出現(xiàn)亂碼,Servlet中獲取HTTP請(qǐng)求參數(shù)出現(xiàn)亂碼,JDBC查詢到的數(shù)據(jù)亂碼等等,這些問(wèn)題很常見(jiàn),遇到的時(shí)候隨手搜一下都可以順利解決,所以沒(méi)有深入的去了解。
直到前兩天同學(xué)與我談起一個(gè)Java源文件的編碼問(wèn)題(這問(wèn)題在最后一個(gè)實(shí)例分析),從這個(gè)問(wèn)題入手拉扯出了一連串的問(wèn)題,然后我們一邊查資料一邊討論,直到深夜,終于在一篇博客中找到了關(guān)鍵性線索,解決了所有的疑惑,以前沒(méi)有理解的語(yǔ)句都能解釋清楚了。因此我決定用這篇隨筆,記錄我對(duì)一些編碼問(wèn)題的理解以及實(shí)驗(yàn)的結(jié)果。
二、概念總結(jié)
早期,互聯(lián)網(wǎng)還沒(méi)有發(fā)展起來(lái),計(jì)算機(jī)僅用于處理一些本地的資料,所以很多國(guó)家和地區(qū)針對(duì)本土的語(yǔ)言設(shè)計(jì)了編碼方案,這種與區(qū)域相關(guān)的編碼統(tǒng)稱為ANSI編碼(因?yàn)槎际菍?duì)ANSI-ASCII碼的擴(kuò)展)。但是他們沒(méi)有事先商量好怎么相互兼容,而是自己搞自己的,這樣就埋下了編碼沖突的禍根,比如大陸使用的GB2312編碼與臺(tái)灣使用的Big5編碼就有沖突,同樣的兩個(gè)字節(jié),在兩種編碼方案里表示的是不同的字符,隨著互聯(lián)網(wǎng)的興起,一個(gè)文檔里經(jīng)常會(huì)包含多種語(yǔ)言,計(jì)算機(jī)在顯示的時(shí)候就遇到麻煩了,因?yàn)樗恢肋@兩個(gè)字節(jié)到底屬于哪種編碼。
這樣的問(wèn)題在世界上普遍存在,因此重新定義一個(gè)通用的字符集,為世界上所有字符進(jìn)行統(tǒng)一編號(hào)的呼聲不斷高漲。
由此Unicode碼應(yīng)運(yùn)而生,它為世界上所有字符進(jìn)行了統(tǒng)一編號(hào),由于它可以唯一標(biāo)識(shí)一個(gè)字符,所以字體也只需要針對(duì)Unicode碼進(jìn)行設(shè)計(jì)就行了。但Unicode標(biāo)準(zhǔn)定義的是一個(gè)字符集,而沒(méi)有規(guī)定編碼方案,也就是說(shuō)它僅僅定義了一個(gè)個(gè)抽象的數(shù)字與其對(duì)應(yīng)的字符,而沒(méi)有規(guī)定具體怎么存儲(chǔ)一串Unicode數(shù)字,真正規(guī)定怎么存儲(chǔ)的是UTF-8、UTF-16、UTF-32等方案,所以帶有UTF開(kāi)頭的編碼,都是可以直接通過(guò)計(jì)算和Unicode數(shù)值(CodePoint,代碼點(diǎn))進(jìn)行轉(zhuǎn)換的。顧名思義,UTF-8就是8位長(zhǎng)度為基本單位編碼,它是變長(zhǎng)編碼,用1~6個(gè)字節(jié)來(lái)編碼一個(gè)字符(因?yàn)槭躑nicode范圍的約束,所以實(shí)際最大只有4字節(jié));UTF-16是16位為基本單位編碼,也是變長(zhǎng)編碼,要么2個(gè)字節(jié)要么4個(gè)字節(jié);UTF-32則是定長(zhǎng)的,固定4字節(jié)存儲(chǔ)一個(gè)Unicode數(shù)。
其實(shí)我以前一直對(duì)Unicode有點(diǎn)誤解,在我的印象中Unicode碼最大只能到0xFFFF,也就是最多只能表示2^16個(gè)字符,在仔細(xì)看了維基百科之后才明白,早期的UCS-2編碼方案確實(shí)是這樣,UCS-2固定使用兩個(gè)字節(jié)來(lái)編碼一個(gè)字符,因此它只能編碼BMP(基本多語(yǔ)言平面,即0x0000-0xFFFF,包含了世界上最常用的字符)范圍內(nèi)的字符。為了要編碼Unicode大于0xFFFF的字符,人們對(duì)UCS-2編碼進(jìn)行了拓展,創(chuàng)造了UTF-16編碼,它是變長(zhǎng)的,在BMP范圍內(nèi),UTF-16與UCS-2完全一致,而B(niǎo)MP之外UTF-16則使用4個(gè)字節(jié)來(lái)存儲(chǔ)。
為了方便下面的描述,先交代一下代碼單元(CodeUnit)的概念,某種編碼的基本組成單位就叫代碼單元,比如UTF-8的代碼單元為1個(gè)字節(jié),UTF-16的代碼單元為2個(gè)字節(jié),不好解釋,但是很好理解。
為了兼容各種語(yǔ)言以及更好的跨平臺(tái),JavaString保存的就是字符的Unicode碼。它以前使用的是UCS-2編碼方案來(lái)存儲(chǔ)Unicode,后來(lái)發(fā)現(xiàn)BMP范圍內(nèi)的字符不夠用了,但是出于內(nèi)存消耗和兼容性的考慮,并沒(méi)有升到UCS-4(即UTF-32,固定4字節(jié)編碼),而是采用了上面所說(shuō)的UTF-16,char類型可看作其代碼單元。這個(gè)做法導(dǎo)致了一些麻煩,如果所有字符都在BMP范圍內(nèi)還沒(méi)事,若有BMP外的字符,就不再是一個(gè)代碼單元對(duì)應(yīng)一個(gè)字符了,length方法返回的是代碼單元的個(gè)數(shù),而不是字符的個(gè)數(shù),charAt方法返回的自然也是一個(gè)代碼單元而不是一個(gè)字符,遍歷起來(lái)也變得麻煩,雖然提供了一些新的操作方法,總歸還是不方便,而且還不能隨機(jī)訪問(wèn)。
此外,我發(fā)現(xiàn)Java在編譯的時(shí)候還不會(huì)處理大于0xFFFF的Unicode字面量,所以如果你敲不出某個(gè)非BMP字符來(lái),但是你知道它的Unicode碼,得用一個(gè)比較笨的方法來(lái)讓String存儲(chǔ)它:手動(dòng)計(jì)算出該字符的UTF-16編碼(四字節(jié)),把前兩個(gè)字節(jié)和后兩個(gè)字節(jié)各作為一個(gè)Unicode數(shù),然后賦值給String,示例代碼如下所示。
public static void main(String[] args) { //String str = ""; //我們想賦值這樣一個(gè)字符,假設(shè)我輸入法打不出來(lái) //但我知道它的Unicode是0x1D11E //String str = "\u1D11E"; //這樣寫不會(huì)識(shí)別 //于是通過(guò)計(jì)算得到其UTF-16編碼 D834 DD1E String str = "\uD834\uDD1E"; //然后這么寫 System.out.println(str); //成功輸出了"" }
Windows系統(tǒng)自帶的記事本可以另存為Unicode編碼,實(shí)際上指的是UTF-16編碼。上面說(shuō)了,主要使用的字符編碼都在BMP范圍內(nèi),而在BMP范圍內(nèi),每個(gè)字符的UTF-16編碼值與對(duì)應(yīng)的Unicode數(shù)值是相等的,這大概就是微軟把它稱為Unicode的原因吧。舉個(gè)例子,我在記事本中輸入了”好a“兩個(gè)字符,然后另存為Unicode big endian(高位優(yōu)先)編碼,用WinHex打開(kāi)文件,內(nèi)容如下圖,文件開(kāi)頭兩個(gè)字節(jié)被稱為Byte Order Mark(字節(jié)順序標(biāo)記),(FE FF)標(biāo)識(shí)字節(jié)序?yàn)楦呶粌?yōu)先,然后(59 7D)正是”好“的Unicode碼,(00 61)正是”a“的Unicode碼。
有了Unicode碼,也還不能立即解決問(wèn)題,因?yàn)槭紫仁澜缟弦呀?jīng)存在了大量的非Unicode標(biāo)準(zhǔn)的編碼數(shù)據(jù),我們不可能丟棄它們,其次Unicode的編碼往往比ANSI編碼更占空間,所以從節(jié)約資源的角度來(lái)說(shuō),ANSI編碼還是有存在的必要的。所以需要建立一個(gè)轉(zhuǎn)換機(jī)制,使得ANSI編碼可以轉(zhuǎn)換到Unicode進(jìn)行統(tǒng)一處理,也可以把Unicode轉(zhuǎn)換到ANSI編碼以適應(yīng)平臺(tái)的要求。
轉(zhuǎn)換方法說(shuō)起來(lái)比較容易,對(duì)于UTF系列或者是ISO-8859-1這種被兼容的編碼,可以通過(guò)計(jì)算和Unicode數(shù)值直接進(jìn)行轉(zhuǎn)換(實(shí)際可能也是查表),而對(duì)于系統(tǒng)遺留下來(lái)的ANSI編碼,則只能通過(guò)查表的方式進(jìn)行,微軟把這種映射表稱為CodePage(代碼頁(yè)),并按編碼進(jìn)行分類編號(hào),比如我們常見(jiàn)的cp936就是GBK的代碼頁(yè),cp65001就是UTF-8的代碼頁(yè)。下圖是微軟官網(wǎng)查到的GBK->Unicode映射表(目測(cè)不全),同理還應(yīng)有反向的Unicode->GBK映射表。
有了代碼頁(yè),就可以很方便的進(jìn)行各種編碼轉(zhuǎn)換了,比如從GBK轉(zhuǎn)換到UTF-8,只需要先按照GBK的編碼規(guī)則對(duì)數(shù)據(jù)按字符劃分,用每個(gè)字符的編碼數(shù)據(jù)去查GBK代碼頁(yè),得到其Unicode數(shù)值,再用該Unicode去查UTF-8的代碼頁(yè)(或直接計(jì)算),就可以得到對(duì)應(yīng)的UTF-8編碼。反過(guò)來(lái)同理。注意:UTF-8是Unicode的標(biāo)準(zhǔn)實(shí)現(xiàn),它的代碼頁(yè)中包含了所有的Unicode取值,所以任意編碼轉(zhuǎn)換到UTF-8,再轉(zhuǎn)換回去都不會(huì)有任何丟失。至此,我們可以得出一個(gè)結(jié)論就是,要完成編碼轉(zhuǎn)換工作,最重要的是第一步要成功的轉(zhuǎn)換到Unicode,所以正確選擇字符集(代碼頁(yè))是關(guān)鍵。
理解了轉(zhuǎn)碼丟失問(wèn)題的本質(zhì)后,我才突然明白JSP的框架為什么要以ISO-8859-1去解碼HTTP請(qǐng)求參數(shù),導(dǎo)致我們獲取中文參數(shù)的時(shí)候不得不寫這樣的語(yǔ)句:
Stringparam=newString(s.getBytes("iso-8859-1"),"UTF-8");
因?yàn)镴SP框架接收到的是參數(shù)編碼的二進(jìn)制字節(jié)流,它不知道這究竟是什么編碼(或者不關(guān)心),也就不知道該查哪個(gè)代碼頁(yè)去轉(zhuǎn)換到Unicode。然后它就選擇了一種絕對(duì)不會(huì)產(chǎn)生丟失的方案,它假設(shè)這是ISO-8859-1編碼的數(shù)據(jù),然后查ISO-8859-1的代碼頁(yè),得到Unicode序列,因?yàn)镮SO-8859-1是按字節(jié)編碼的,而且不同于ASCII的是,它對(duì)0~255空間的每一位都進(jìn)行了編碼,所以任意一個(gè)字節(jié)都能在它的代碼頁(yè)中找到對(duì)應(yīng)的Unicode,若再?gòu)腢nicode轉(zhuǎn)回原始字節(jié)流的話也就不會(huì)有任何丟失。它這樣做,對(duì)于不考慮其他語(yǔ)言的歐美程序員來(lái)說(shuō),可以直接用JSP框架解碼好的String,而要兼容其他語(yǔ)言的話也只需要轉(zhuǎn)回原始字節(jié)流,再以實(shí)際的代碼頁(yè)去解碼一下就好。
我對(duì)Unicode以及字符編碼的相關(guān)概念闡述完畢,接下來(lái)用Java實(shí)例來(lái)感受一下。
三、實(shí)例分析
1.轉(zhuǎn)換到Unicode——String構(gòu)造方法
String的構(gòu)造方法就是把各種編碼數(shù)據(jù)轉(zhuǎn)換到Unicode序列(以UTF-16編碼存儲(chǔ)),下面這段測(cè)試代碼,用來(lái)展示JavaString構(gòu)造方法的應(yīng)用,實(shí)例中都不涉及非BMP字符,所以就不用codePointAt那些方法了。
public class Test { public static void main(String[] args) throws IOException { //"你好"的GBK編碼數(shù)據(jù) byte[] gbkData = {(byte)0xc4, (byte)0xe3, (byte)0xba, (byte)0xc3 } ; //"你好"的BIG5編碼數(shù)據(jù) byte[] big5Data = {(byte)0xa7, (byte)0x41, (byte)0xa6, (byte)0x6e } ; //構(gòu)造String,解碼為Unicode String strFromGBK = new String(gbkData, "GBK"); String strFromBig5 = new String(big5Data, "BIG5"); //分別輸出Unicode序列 showUnicode(strFromGBK); showUnicode(strFromBig5); } public static void showUnicode(String str) { for (int i = 0; i < str.length(); i++) { System.out.printf("\\u%x", (int)str.charAt(i)); } System.out.println(); } }
運(yùn)行結(jié)果如下圖
可以發(fā)現(xiàn),由于String掌握了Unicode碼,要轉(zhuǎn)換到其它編碼soeasy!
3.以Unicode為橋梁,實(shí)現(xiàn)編碼互轉(zhuǎn)
有了上面兩部分的基礎(chǔ),要實(shí)現(xiàn)編碼互轉(zhuǎn)就很簡(jiǎn)單了,只需要把他們聯(lián)合使用就可以了。先newString把原編碼數(shù)據(jù)轉(zhuǎn)換為Unicode序列,再調(diào)用getBytes轉(zhuǎn)到指定的編碼就OK。
比如一個(gè)很簡(jiǎn)單的GBK到Big5的轉(zhuǎn)換代碼如下
public static void main(String[] args) throws UnsupportedEncodingException { //假設(shè)這是以字節(jié)流方式從文件中讀取到的數(shù)據(jù)(GBK編碼) byte[] gbkData = {(byte) 0xc4, (byte) 0xe3, (byte) 0xba, (byte) 0xc3 } ; //轉(zhuǎn)換到Unicode String tmp = new String(gbkData, "GBK"); //從Unicode轉(zhuǎn)換到Big5編碼 byte[] big5Data = tmp.getBytes("Big5"); //后續(xù)操作…… }
4.編碼丟失問(wèn)題
上面已經(jīng)解釋了,JSP框架采用ISO-8859-1字符集來(lái)解碼的原因。先用一個(gè)例子來(lái)模擬這個(gè)還原過(guò)程,代碼如下
public class Test { public static void main(String[] args) throws UnsupportedEncodingException { //JSP框架收到6個(gè)字節(jié)的數(shù)據(jù) byte[] data = {(byte) 0xe4, (byte) 0xbd, (byte) 0xa0, (byte) 0xe5, (byte) 0xa5, (byte) 0xbd } ; //打印原始數(shù)據(jù) showBytes(data); //JSP框架假設(shè)它是ISO-8859-1的編碼,生成一個(gè)String對(duì)象 String tmp = new String(data, "ISO-8859-1"); //**************JSP框架部分結(jié)束******************** //開(kāi)發(fā)者拿到后打印它發(fā)現(xiàn)是6個(gè)歐洲字符,而不是預(yù)期的"你好" System.out.println(" ISO解碼的結(jié)果:" + tmp); //因此首先要得到原始的6個(gè)字節(jié)的數(shù)據(jù)(反查ISO-8859-1的代碼頁(yè)) byte[] utfData = tmp.getBytes("ISO-8859-1"); //打印還原的數(shù)據(jù) showBytes(utfData); //開(kāi)發(fā)者知道它是UTF-8編碼的,因此用UTF-8的代碼頁(yè),重新構(gòu)造String對(duì)象 String result = new String(utfData, "UTF-8"); //再打印,正確了! System.out.println(" UTF-8解碼的結(jié)果:" + result); } public static void showBytes(byte[] data) { for (byte b : data) System.out.printf("0x%x ", b); System.out.println(); } }
運(yùn)行結(jié)果如下,第一次輸出是不正確的,因?yàn)榻獯a規(guī)則不對(duì),也查錯(cuò)了代碼頁(yè),得到的是錯(cuò)誤的Unicode。然后發(fā)現(xiàn)通過(guò)錯(cuò)誤的Unicode反查ISO-8859-1代碼頁(yè)還能完美的還原數(shù)據(jù)。
這不是重點(diǎn),重點(diǎn)如果把“中”換成“中國(guó)”,編譯就會(huì)成功,運(yùn)行結(jié)果如下圖。另外進(jìn)一步可發(fā)現(xiàn),中文字符個(gè)數(shù)為奇數(shù)時(shí)編譯失敗,偶數(shù)時(shí)通過(guò)。這是為什么呢?下面詳細(xì)分析一下。
因?yàn)镴avaString內(nèi)部使用的是Unicode,所以在編譯的時(shí)候,編譯器就會(huì)對(duì)我們的字符串字面量進(jìn)行轉(zhuǎn)碼,從源文件的編碼轉(zhuǎn)換到Unicode(維基百科說(shuō)用的是與UTF-8稍微有點(diǎn)不同的編碼)。編譯的時(shí)候我們沒(méi)有指定encoding參數(shù),所以編譯器會(huì)默認(rèn)以GBK方式去解碼,對(duì)UTF-8和GBK有點(diǎn)了解的應(yīng)該會(huì)知道,一般一個(gè)中文字符使用UTF-8編碼需要3個(gè)字節(jié),而GBK只需要2個(gè)字節(jié),這就能解釋為什么字符數(shù)的奇偶性會(huì)影響結(jié)果,因?yàn)槿绻?個(gè)字符,UTF-8編碼占6個(gè)字節(jié),以GBK方式來(lái)解碼恰好能解碼為3個(gè)字符,而如果是1個(gè)字符,就會(huì)多出一個(gè)無(wú)法映射的字節(jié),就是圖中問(wèn)號(hào)的地方。
再具體一點(diǎn)的話,源文件中“中國(guó)”二字的UTF-8編碼是e4b8ade59bbd,編譯器以GBK方式解碼,3個(gè)字節(jié)對(duì)分別查cp936得到3個(gè)Unicode值,分別是6d93e15e6d57,對(duì)應(yīng)結(jié)果圖中的三個(gè)奇怪字符。如下圖所示,編譯后這3個(gè)Unicode在.class文件中實(shí)際以類UTF-8編碼存儲(chǔ),運(yùn)行的時(shí)候,JVM中存儲(chǔ)的就是Unicode,然而最終輸出時(shí),還是會(huì)編碼之后傳遞給終端,這次約定的編碼就是系統(tǒng)區(qū)域設(shè)置的編碼,所以如果終端編碼設(shè)置改了,還是會(huì)亂碼。我們這里的e15e在Unicode標(biāo)準(zhǔn)中并沒(méi)有定義相應(yīng)的字符,所以在不同平臺(tái)不同字體下顯示會(huì)有所不同。
可以想象,如果反過(guò)來(lái),源文件以GBK編碼存儲(chǔ),然后騙編譯器說(shuō)是UTF-8,那基本上是無(wú)論輸入多少個(gè)中文字符都無(wú)法編譯通過(guò)了,因?yàn)閁TF-8的編碼很有規(guī)律性,隨意組合的字節(jié)是不會(huì)符合UTF-8編碼規(guī)則的。
當(dāng)然,要使編譯器能正確的把編碼轉(zhuǎn)換到Unicode,最直接的方法還是老老實(shí)實(shí)告訴編譯器源文件的編碼是什么。
感謝你能夠認(rèn)真閱讀完這篇文章,希望小編分享的“Java中ANSI,Unicode,BMP,UTF等編碼概念的示例分析”這篇文章對(duì)大家有幫助,同時(shí)也希望大家多多支持億速云,關(guān)注億速云行業(yè)資訊頻道,更多相關(guān)知識(shí)等著你來(lái)學(xué)習(xí)!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。