前言
近日遇到一個(gè)神奇的問題疫蔓,在github上找了開源庫,使用如下兩種不同的運(yùn)行方式得到的運(yùn)行結(jié)果也不同身冬,但理論上運(yùn)行結(jié)果應(yīng)該是一致的才是衅胀。
方式一: 將源碼編譯為jar包,使用java -jar xxx 的方式運(yùn)行得到的運(yùn)行結(jié)果是正常的酥筝。(源碼作者使用的方式)
-
方式二: 將源代碼導(dǎo)入IDEA中滚躯,使用IDEA來運(yùn)行main()的方式得到的運(yùn)行結(jié)果是異常的。
以上兩種運(yùn)行的方式從理論上沒有什么差別的,那么為何會(huì)得出不同的運(yùn)行結(jié)果呢掸掏?在經(jīng)過查閱資料和同事的討論以及一系列的驗(yàn)證后終于找出問題的根本原因----Java編譯茁影、運(yùn)行過程中的字符編碼導(dǎo)致的。
1丧凤、字符編碼與解碼
1.1募闲、基本概念
在了解字符編碼與解碼之前我們先需要知道字符集的概念。字符是用戶可以讀寫的最小單位愿待,計(jì)算機(jī)所能支持的字符組成的集合浩螺,就叫做字符集。字符集通常以二維表的形式存在呼盆。二維表的內(nèi)容和大小是由使用者的語言而定年扩。
顧名思義,編碼就是把一個(gè)字符編碼成二進(jìn)制碼存起來的方式访圃。相應(yīng)的,將編碼的字節(jié)還原成字符的操作就叫做解碼相嵌。編碼和解碼都是需要按照一定的規(guī)則腿时,把字符集中的字符編碼為特定的二進(jìn)制數(shù)的規(guī)則就是字符編碼。
1.2饭宾、為什么要編碼批糟?
由于人類的語言太多,因而表示這些語言的符號(hào)太多看铆,無法使用計(jì)算機(jī)中一個(gè)基本存儲(chǔ)單位--字節(jié) 來表示徽鼎,因而必須要經(jīng)過拆分或一些翻譯工作,才能讓計(jì)算機(jī)理解弹惦。計(jì)算機(jī)中一個(gè)字節(jié)所能表示的字符范圍是0-255個(gè)否淤。人類要表示的符號(hào)太多,無法使用一個(gè)字節(jié)來完全表示棠隐,這就需要使用編碼來解決這個(gè)問題石抡。
1.3、常見的編碼格式
1.3.1助泽、 ASCII碼
上世紀(jì)60年代啰扛,美國制定了一套字符編碼,對(duì)英語字符與二進(jìn)制之間的關(guān)系嗡贺,做了統(tǒng)一規(guī)定隐解。這被稱為ASCII碼。ASCII碼一共規(guī)定了128個(gè)字符的編碼诫睬,用一個(gè)字節(jié)的低7位表示煞茫,最高位統(tǒng)一規(guī)定為0。0-31是控制字符如換行、回車等溜嗜;32-126是打印字符宵膨,可以通過鍵盤輸入并且能夠顯示出來。
1.3.2炸宵、 ISO-8859-1
128個(gè)字符顯然是不夠用的辟躏,于是ISO組織在ASCII碼基礎(chǔ)上又制定了一系列標(biāo)準(zhǔn)用來擴(kuò)展ASCII碼,ISO-8859-1涵蓋了大多數(shù)西歐語言字符土全。ISO-8859-1仍然是單字節(jié)編碼捎琐,它總共能表示256個(gè)字符。
1.3.3裹匙、 GBK
全稱叫《漢字內(nèi)碼擴(kuò)展規(guī)范》瑞凑,是國家技術(shù)監(jiān)督局為windows95制定的新的漢字內(nèi)碼規(guī)范,它的出現(xiàn)是為了擴(kuò)展GB2312,加入更多的漢字概页。
1.3.4籽御、 Unicode
隨著計(jì)算機(jī)的發(fā)展,各國都推出各自的編碼標(biāo)準(zhǔn)惰匙,互不兼容技掏,非常不利于全球化發(fā)展。于是Unicode誕生了项鬼,它為每種語言中的每個(gè)字符設(shè)定了統(tǒng)一并且唯一的二進(jìn)制編碼哑梳,計(jì)算機(jī)只要支持這一個(gè)字符集,就能顯示所有的字符绘盟,再也不會(huì)有亂碼了鸠真。Unicode理論上最多能表示2的31次方個(gè)字符,完全可以涵蓋一切語言所用的符號(hào)龄毡。
對(duì)于 Unicode 有一些誤解吠卷,它僅僅只是一個(gè)字符集,規(guī)定了符合對(duì)應(yīng)的二進(jìn)制代碼稚虎,至于這個(gè)二進(jìn)制代碼如何存儲(chǔ)則沒有任何規(guī)定撤嫩,所以這也造成了一些問題。比如蠢终,漢字"嚴(yán)"的 Unicode 是十六進(jìn)制數(shù)4E25序攘,轉(zhuǎn)換成二進(jìn)制數(shù)足足有15位(100111000100101),也就是說寻拂,這個(gè)符號(hào)的表示至少需要2個(gè)字節(jié)程奠。表示其他更大的符號(hào),可能需要3個(gè)字節(jié)或者4個(gè)字節(jié)祭钉,甚至更多瞄沙。
因?yàn)橹辽傩枰?個(gè)字節(jié)來表示更大的符號(hào),這就導(dǎo)致了兩個(gè)問題:
1.如何區(qū)別該編碼是Unicode還是ASCII,計(jì)算機(jī)怎么知道該字符是2個(gè)字節(jié)還是3個(gè)字節(jié)甚至更多距境。
2.眾所周知申尼,英文字母只需要一個(gè)字節(jié)來進(jìn)行編碼,但是如果用2個(gè)字節(jié)3個(gè)字節(jié)甚至更多字節(jié)來表示這就會(huì)造成相應(yīng)倍數(shù)的存儲(chǔ)空間的增加垫桂,造成了存儲(chǔ)空間上的極大浪費(fèi)师幕。
所以最后也出現(xiàn)了Unicode的多種存儲(chǔ)方式,也就是說有許多種不同的二進(jìn)制格式來表示Unicode诬滩。
Unicode的實(shí)現(xiàn)方式也稱為Unicode轉(zhuǎn)換格式(Unicode Transformation Format,簡稱UTF)霹粥,目前主流的實(shí)現(xiàn)方式有UTF-8和UTF-16。以下就分別介紹UTF-8和UTF-16疼鸟。
1.3.5后控、 UTF-8
UTF-8是針對(duì)Unicode的一種可變長度字符編碼。它可以用來表示Unicode標(biāo)準(zhǔn)中的任何字符空镜,因而其編碼中的第一個(gè)字節(jié)仍與ASCII相容浩淘。UTF-8使用1~4字節(jié)為每個(gè)字符編碼,根據(jù)不同的字符而變化字節(jié)長度姑裂。
編碼規(guī)則如下:
Unicode 十六進(jìn)制碼點(diǎn)范圍 | UTF-8二進(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 |
根據(jù)上表可得出如下結(jié)論:
1.如果一個(gè)字節(jié)馋袜,最高位(第八位)為0,表示這是一個(gè)ASCII字符舶斧。可見察皇,所有的ASCII編碼已經(jīng)是UTF-8編碼茴厉。
2.如果一個(gè)字節(jié)以11開頭,連續(xù)的1的個(gè)數(shù)暗示這個(gè)字符的字節(jié)數(shù)什荣,例如:110xxxxx代表它是雙字節(jié)UTF-8字符的首字節(jié)矾缓。
3.如果一個(gè)字節(jié),是以10開始稻爬,表示它不是首字節(jié)嗜闻,需要向前查找才能得到當(dāng)前字符的首字節(jié)。
例子:
- "漢"的 Unicode 碼點(diǎn)是 0x6c49(110 1100 0100 1001)桅锄,通過上面的對(duì)照表可以發(fā)現(xiàn)琉雳,0x0000 6c49 位于第三行的范圍,那么得出其格式為 1110xxxx 10xxxxxx 10xxxxxx友瘤。接著翠肘,從“漢”的二進(jìn)制數(shù)最后一位開始,從后向前依次填充對(duì)應(yīng)格式中的 x辫秧,多出的 x 用 0 補(bǔ)上束倍。這樣,就得到了“漢”的 UTF-8 編碼為 11100110 10110001 10001001。由此可以看出”漢“使用UTF-8編碼后是使用三個(gè)字節(jié)表示绪妹。
- "A" 的 Unicode 碼點(diǎn)是 0x0041 ( 0100 0001)甥桂,通過上面的對(duì)照表可以發(fā)現(xiàn),0x0041位于第一行的范圍邮旷,那么得出其格式為0xxxxxx黄选。故”A"的UTF-8編碼為01000001±纫疲可以看出“A"使用UTF-8編碼后是使用單字節(jié)表示糕簿。
1.3.4.2、 UTF-16
UTF-16是Unicode字符編碼表的一種實(shí)現(xiàn)方式狡孔。即把Unicode字符集的抽象碼位映射為16位長的整數(shù)(即碼元懂诗,長度為2byte)的序列引用,用于數(shù)據(jù)存儲(chǔ)或傳遞苗膝。Unicode字符碼位殃恒,需要1個(gè)或者2個(gè)16位長的碼元表示,因此這是一個(gè)變長表示辱揭。
在了解 UTF-16 編碼方式之前离唐,先了解一下另外一個(gè)概念——"平面"。
在上面的介紹中问窃,提到了 Unicode 是一本很厚的字典亥鬓,她將全世界所有的字符定義在一個(gè)集合里。這么多的字符不是一次性定義的域庇,而是分區(qū)定義嵌戈。每個(gè)區(qū)可以存放 65536 個(gè)(2^16) 字符,稱為一個(gè)平面(plane)听皿。目前熟呛,一共有17個(gè)(2^5)平面,也就是說尉姨,整個(gè) Unicode 字符集的大小現(xiàn)在是 2^21庵朝。
最前面的 65536 個(gè)字符位,稱為基本平面(簡稱 BMP )又厉,它的碼點(diǎn)范圍是從 0 到 2^16-1九府,寫成 16 進(jìn)制就是從 U+0000 到 U+FFFF。所有最常見的字符都放在這個(gè)平面馋没,這是 Unicode 最先定義和公布的一個(gè)平面昔逗。剩下的字符都放在輔助平面(簡稱 SMP ),碼點(diǎn)范圍從 U+010000 到 U+10FFFF篷朵。
基本了解了平面的概念后勾怒,再說回到 UTF-16婆排。UTF-16 編碼介于 UTF-32 與 UTF-8 之間,同時(shí)結(jié)合了定長和變長兩種編碼方法的特點(diǎn)笔链。它的編碼規(guī)則很簡單:基本平面的字符占用 2 個(gè)字節(jié)段只,輔助平面的字符占用 4 個(gè)字節(jié)。也就是說鉴扫,UTF-16 的編碼長度要么是 2 個(gè)字節(jié)(U+0000 到 U+FFFF)赞枕,要么是 4 個(gè)字節(jié)(U+010000 到 U+10FFFF)。那么問題來了坪创,當(dāng)我們遇到兩個(gè)字節(jié)時(shí)炕婶,到底是把這兩個(gè)字節(jié)當(dāng)作一個(gè)字符還是與后面的兩個(gè)字節(jié)一起當(dāng)作一個(gè)字符呢?
這里有一個(gè)很巧妙的地方莱预,在基本平面內(nèi)柠掂,從 U+D800 到 U+DFFF 是一個(gè)空段,即這些碼點(diǎn)不對(duì)應(yīng)任何字符依沮。因此涯贞,這個(gè)空段可以用來映射輔助平面的字符。
輔助平面的字符位共有 2^20 個(gè)危喉,因此表示這些字符至少需要 20 個(gè)二進(jìn)制位宋渔。UTF-16 將這 20 個(gè)二進(jìn)制位分成兩半,前 10 位映射在 U+D800 到 U+DBFF辜限,稱為高位(H)皇拣,后 10 位映射在 U+DC00 到 U+DFFF,稱為低位(L)薄嫡。這意味著审磁,一個(gè)輔助平面的字符,被拆成兩個(gè)基本平面的字符表示岂座。
因此,當(dāng)我們遇到兩個(gè)字節(jié)杭措,發(fā)現(xiàn)它的碼點(diǎn)在 U+D800 到 U+DBFF 之間费什,就可以斷定,緊跟在后面的兩個(gè)字節(jié)的碼點(diǎn)手素,應(yīng)該在 U+DC00 到 U+DFFF 之間鸳址,這四個(gè)字節(jié)必須放在一起解讀。
UTF-16編碼以16位無符號(hào)整數(shù)為單位泉懦。我們把Unicode編碼記作U稿黍。編碼規(guī)則如下:
平面 | Unicode 十六進(jìn)制碼點(diǎn)范圍 | UTF-16 二進(jìn)制 |
---|---|---|
基本平面 | 0000 0000 - 0000 FFFF | xxxx xxxx xxxx xxxx |
增補(bǔ)平面 | 0001 0000 - 0010 FFFF | 1101 10yy yyyy yyyy 1101 11xx xxxx xxxx |
- 如果U<0x10000,U的UTF-16編碼就是U對(duì)應(yīng)的16位無符號(hào)整數(shù)崩哩。
- 如果U≥0x10000巡球,我們先計(jì)算U'=U-0x10000言沐,然后將U'寫成二進(jìn)制形式:yyyy yyyy yyxx xxxx xxxx,U的UTF-16編碼(二進(jìn)制)就是:110110yyyyyyyyyy 110111xxxxxxxxxx酣栈。
對(duì)于輔助平面的字符险胰,由于超過了一個(gè)16位可以表示的長度,所以需要兩個(gè)16位來表示矿筝。處于前面的16位被稱為前導(dǎo)起便,而后面的被稱為后綴。所以UTF-16要么是2字節(jié)窖维,要么是4字節(jié)榆综。
例子:
- ?? (不是吉)的Unicode碼點(diǎn)為 0x20BB7,該碼點(diǎn)顯然超出了基本平面的范圍(0x0000 - 0xFFFF)铸史,因此需要使用四個(gè)字節(jié)表示鼻疮。首先用 0x20BB7 - 0x10000 計(jì)算出超出的部分,然后將其用 20 個(gè)二進(jìn)制位表示(不足前面補(bǔ) 0 )沛贪,結(jié)果為0001000010 1110110111陋守。接著,將前 10 位映射到 U+D800 到 U+DBFF 之間利赋,后 10 位映射到 U+DC00 到 U+DFFF 即可水评。這樣得到的結(jié)果就是1101 1000 0100 0010 1101 1111 1011 0111。由此可以看出”吉“使用UTF-16編碼后是使用四個(gè)字節(jié)媚送。
- 什 的Unicode碼點(diǎn)為0x4EC0中燥,該碼點(diǎn)在基本平面(0x0000 - 0xFFFF),因此僅需要兩個(gè)字節(jié)表示。即為0100 1110 1100 0000塘偎。
2疗涉、出現(xiàn)亂碼的原因
出現(xiàn)亂碼問題唯一的原因都是在char到byte或byte到char轉(zhuǎn)換中編碼和解碼的字符集不一致導(dǎo)致的,由于往往一次操作涉及到多次編解碼吟秩,所以出現(xiàn)亂碼時(shí)很難查找到底是哪個(gè)環(huán)節(jié)出現(xiàn)了問題咱扣。下面是幾種常見的現(xiàn)象:
- 現(xiàn)象一: 解碼時(shí)用的字符集與編碼字符集不一致導(dǎo)致亂碼。
如上圖所示:字符串"淘涵防!我喜歡闹伪!"編碼是采用GBK,但解碼時(shí)采用ISO-8859-I,這樣就會(huì)導(dǎo)致亂碼壮池。
- 現(xiàn)象二: 字符在編碼時(shí)采用錯(cuò)誤的字符集編碼導(dǎo)致亂碼偏瓤。(如中文采用ISO-8859-I編碼方式)
使用錯(cuò)誤的編碼方式.png
這種情況比較復(fù)雜,中文經(jīng)過多次編碼橙依,但其中有一次編碼或解碼不對(duì)证舟,就會(huì)出現(xiàn)亂碼硕旗。
3、java編譯褪储、運(yùn)行過程中的字符編碼
java在編譯和運(yùn)行的整個(gè)過程中編碼轉(zhuǎn)化大概如下圖:
可以看到Java運(yùn)行時(shí)主要的兩個(gè)編碼就是UTF-8和UTF-16,而編譯的開始,就是將各種不同編碼的源代碼文件轉(zhuǎn)換成UTF-8鲤竹。
這里其實(shí)并不是UTF-8浪读,是一種modified UTF-8,這里就姑且認(rèn)為是UTF-8辛藻。
從上圖可以理解不管采用那種格式的源文件碘橘,只要正確告訴編譯器源文件的編碼格式,編譯器就會(huì)得到正確的結(jié)果吱肌。同時(shí)只要告訴JVM正確的輸出流需要的編碼格式痘拆,JVM就可以返回正確編碼格式的輸出流。
那么要想不產(chǎn)生亂碼就需要注意如下兩個(gè)環(huán)節(jié):
- 告訴編譯器你java源文件的編碼格式氮墨。
- 告訴jvm你顯示或者構(gòu)造字符串輸出流時(shí)的希望的編碼纺蛆。
3.1、編譯時(shí)的編碼轉(zhuǎn)換
眾所周知规揪,java源文件可以是任意的源碼桥氏,但是在編譯的時(shí)候,javac編譯器默認(rèn)會(huì)使用操作系統(tǒng)平臺(tái)的編碼進(jìn)行解析字符猛铅。在簡體中文的Windows上字支,平臺(tái)默認(rèn)編碼會(huì)是GBK,那么javac就會(huì)默認(rèn)假定輸入的Java源文件是以GBK編碼的奸忽。
要想正確編譯堕伪,需要使用 -encoding指定輸入的java源文件的編碼。
-encoding encoding Set the source file encoding name, such as EUC-JP and UTF-8. If -encoding is not specified, the platform default converter is used.
導(dǎo)致亂碼的不是Java源碼編譯器的“編碼”(寫出UTF-8格式到class文件中)的過程栗菜,而是“解碼”(讀入Java源碼內(nèi)容)的過程欠雌。
3.2、運(yùn)行時(shí)的編碼轉(zhuǎn)換
JVM中運(yùn)行時(shí)數(shù)據(jù)都是使用UTF-16進(jìn)行編碼的疙筹。為什么JVM使用的是UTF-16,而不適用兼容性更好的UTF-8呢桨昙?這是因?yàn)闅v史原因?qū)е翵VM運(yùn)行時(shí)的數(shù)據(jù)使用UTF-16的編碼。
由于成本問題不能放棄UTF-16腌歉,但是UTF-8的兼容性和流行程度,又使得JVM必須做點(diǎn)什么來使得其內(nèi)部數(shù)據(jù)不會(huì)被編碼方式影響齐苛,于是就有了這個(gè)modified UTF-8翘盖。
modified UTF-8是對(duì)UTF-16的再編碼,所以JVM無需解碼UTF-16的數(shù)據(jù)凹蜂,modified UTF-8代理碼元會(huì)處理這個(gè)映射關(guān)系馍驯。
可以在啟動(dòng)JVM時(shí)使用-Dfile.encoding=xxx來設(shè)置阁危。這個(gè)屬性決定了JVM輸出的字節(jié)流編碼格式。
3.3汰瘫、 Java的 file.encoding和sun.jun.encoding的屬性
Java的file.encoding屬性的設(shè)置Jvm運(yùn)行過程中默認(rèn)的字符編碼狂打,比如:new String(bytes)、String.getBytes()混弥、IO操作過程中等所用的默認(rèn)編碼格式都是file.encoding屬性的所決定趴乡。
Java的sun.jun.encoding屬性的主要設(shè)置下面三個(gè)地方的編碼:
- 命令行參數(shù)
- 主類名稱
- 環(huán)境變量
4、開發(fā)中關(guān)于編碼的建議
在我們了解常用的編碼格式以及Java編譯蝗拿、運(yùn)行過程中的默認(rèn)編碼后晾捏,下面是關(guān)于實(shí)際開發(fā)中關(guān)于編碼的一些建議:
- 項(xiàng)目源文件編碼應(yīng)統(tǒng)一為UTF-8,從兼容性哀托、存儲(chǔ)效率惦辛、存儲(chǔ)容量等因素考慮UTF-8是最合適。(據(jù)情形而定仓手,對(duì)于含有大量中文或者其他二字節(jié)長的字符流來說胖齐,UTF-16可以節(jié)省大量的存儲(chǔ)空間)。
- 項(xiàng)目源文件的編碼應(yīng)統(tǒng)一嗽冒,即A.java和B.java的編碼格式應(yīng)統(tǒng)一呀伙。若不一致很容易造成亂碼。
- char與byte彼此轉(zhuǎn)換中應(yīng)指定編碼格式辛慰,不應(yīng)該使用默認(rèn)的編碼格式区匠。如:new String(bytes,"UTF-8")、String.getBytes("UTF-8")帅腌、IO操作中涉及到字符編碼等驰弄。
上述三個(gè)建議,我們最容易忽略的是最后一個(gè)速客,最后一個(gè)也是我們就容易出現(xiàn)的一個(gè)戚篙。
5、總結(jié)
本文最開始描述了工作中遇到的一個(gè)神奇的問題溺职,使得我想去探究Java編譯岔擂、運(yùn)行過程中的編碼格式。正文中首先介紹了字符編碼與解碼的基本概念浪耘,接著介紹了常用的幾種編碼格式乱灵。然后分析了平常出現(xiàn)亂碼問題的原因。最后是本文的核心介紹了Java在編譯七冲、運(yùn)行過程中編碼格式以及如何保證不出現(xiàn)亂碼痛倚。