編碼規(guī)則
如果你已經(jīng)閱讀了JavaHipster 1中references提到的兩篇文章桥胞,你應(yīng)該明白:從字符集到編碼規(guī)則的過(guò)程奋救,實(shí)際上就是從字符集-->到編號(hào)-->到編碼的過(guò)程滤蝠。
關(guān)于編碼規(guī)則稿辙,主要關(guān)注兩個(gè)方面:
- 所遵循的字符集
- 存儲(chǔ)方式:使用幾個(gè)字節(jié)來(lái)存儲(chǔ)字符
ASCII編碼規(guī)則
- 遵循ASCII字符集航厚,共含有255個(gè)字符(有很多字符是保留字符)
- 使用1個(gè)字節(jié)存儲(chǔ)(定長(zhǎng))
ISO8859-1編碼規(guī)則
- 遵循ISO8859-1字符集顷歌,共含有255個(gè)字符。
- 使用1個(gè)字節(jié)存儲(chǔ)(定長(zhǎng))
- ISO8859-1兼容ASCII幔睬,也就是說(shuō)眯漩,對(duì)于同樣的字符,ASCII與ISO8859-1對(duì)應(yīng)的編號(hào)(也成為code point碼點(diǎn))都是一樣的麻顶。
GB2312編碼規(guī)則
- 遵循GB2312字符集赦抖,支持常見(jiàn)的中文。
- 使用1個(gè)字節(jié)存儲(chǔ)英文和數(shù)字等字符辅肾,使用2個(gè)字節(jié)存儲(chǔ)中文字符队萤。
- GB2312兼容ASCII。
GBK編碼規(guī)則
- 遵循GBK字符集矫钓,支持常見(jiàn)的中文要尔,罕見(jiàn)中文,繁體中文新娜,日文的假名赵辕。
- 使用1個(gè)字節(jié)存儲(chǔ)英文和數(shù)字等字符,使用2個(gè)字節(jié)存儲(chǔ)中文字符概龄。
- GBK兼容ASCII與GB2312还惠。
UTF-16編碼規(guī)則
- 遵循Unicode字符集。支持地球上所有常見(jiàn)的自然語(yǔ)言私杜。
- 使用2個(gè)字節(jié)存儲(chǔ)各種語(yǔ)言的常用字符吸重,使用4個(gè)字節(jié)存儲(chǔ)其他罕見(jiàn)字符互拾。
- UTF-16并不兼容ASCII或者ISO8859-1,原因是UTF-16使用2個(gè)字節(jié)表示英文和西歐字符嚎幸。
- 使用UTF-16編碼的文件,在文件頭部均含有BOM寄猩,以說(shuō)明這個(gè)文件使用大端法還是小端法進(jìn)行存儲(chǔ)嫉晶。
- 在有一些文本編輯器中(比如notepad++),會(huì)把UTF-16稱為UCS-2田篇。
UTF-8編碼規(guī)則
- 遵循Unicode字符集替废。支持地球上所有常見(jiàn)的自然語(yǔ)言。
- 使用1個(gè)字節(jié)存儲(chǔ)英文和數(shù)字等字符泊柬,使用3個(gè)字節(jié)存儲(chǔ)常用中文椎镣,使用4個(gè)字符存儲(chǔ)罕見(jiàn)字符。
- UTF-8兼容ASCII兽赁,但不兼容UTF-16状答,因?yàn)閁TF-8存儲(chǔ)字符所需的空間與UTF-16是不一樣的。
- UTF-8默認(rèn)不帶BOM刀崖。最好也不要加上BOM惊科。
ANSI
ANSI嚴(yán)格來(lái)說(shuō)并不是一種編碼規(guī)則,它表示:根據(jù)當(dāng)前操作系統(tǒng)以及操作系統(tǒng)的語(yǔ)言亮钦,選擇對(duì)應(yīng)的編碼規(guī)則進(jìn)行編碼馆截。例如,對(duì)于簡(jiǎn)體中文的Windows操作系統(tǒng)蜂莉,ANSI代表GBK蜡娶。在繁體中文Windows操作系統(tǒng)中,ANSI代表Big5映穗。在日文Windows操作系統(tǒng)中窖张,ANSI代表Shift_JIS 編碼。
文件編碼規(guī)則與JVM編碼規(guī)則
對(duì)于一個(gè)文件而言男公,我們可以顯式的指定文件的編碼規(guī)則荤堪。但當(dāng)文件中的內(nèi)容讀到JVM內(nèi)存中后,將會(huì)采用JVM的編碼規(guī)則進(jìn)行重新編碼和存儲(chǔ)枢赔。例如澄阳,在JVM中,Java的char類型和String類型均強(qiáng)制使用UTF-16進(jìn)行編碼踏拜。因此對(duì)于一個(gè)非UTF-16的文件來(lái)說(shuō)碎赢,在數(shù)據(jù)存儲(chǔ)層面,數(shù)據(jù)依舊采用文件本身的編碼規(guī)則進(jìn)行編碼和存儲(chǔ)速梗。但當(dāng)數(shù)據(jù)讀到JVM中肮塞,將采用UTF-16進(jìn)行編碼和存儲(chǔ)襟齿。
下面的例子將解釋:文件編碼規(guī)則與JVM編碼規(guī)則的區(qū)別
//把下面的代碼分別在UTF-8與UTF-16編碼格式的文件下進(jìn)行測(cè)試
char ch1 = 'c';
Character character1 = new Character(ch1);
System.out.println(character1.SIZE); //(1)
char ch2 = '中';
Character character2 = new Character(ch2);
System.out.println(character2.SIZE); //(2)
String str1 = "a";
System.out.println(str1.length()); //(3)
System.out.println(str1.toCharArray().length); //(4)
System.out.println(str1.getBytes().length); //(5)
String str2 = "中";
System.out.println(str2.length()); //(6)
System.out.println(str2.toCharArray().length); //(7)
System.out.println(str2.getBytes().length); //(8)
Character.SIZE返回的是JVM中char類型占多少bit。對(duì)于(1)和(2)而言枕赵,在UTF8和UTF16環(huán)境下猜欺,打印結(jié)果均是16bit,即兩個(gè)字符拷窜。原因是:無(wú)論文件采用哪種編碼規(guī)則中开皿,在JVM中,char類型均使用2個(gè)字節(jié)存儲(chǔ)字符篮昧。
String.length()返回的是JVM中字符串所對(duì)應(yīng)的代碼單元長(zhǎng)度赋荆。在Java中,字符串是由char字符數(shù)組所組成的懊昨,因此在JVM中字符串也使用UTF16進(jìn)行存儲(chǔ)窄潭。
代碼單元指一種轉(zhuǎn)換格式中最小的一個(gè)分隔,由于UTF16最少使用2個(gè)字節(jié)表示一個(gè)字符酵颁,因此對(duì)于UTF-16而言嫉你,1個(gè)代碼單元等價(jià)于2個(gè)字節(jié)。而對(duì)于UTF-8而言材义,1個(gè)代碼單元等價(jià)于1個(gè)字節(jié)均抽。
對(duì)于(3)和(6)而言,無(wú)論文件使用UTF8還是UTF16進(jìn)行編碼存儲(chǔ)其掂,在JVM中油挥,String均使用UTF-16進(jìn)行存儲(chǔ),因此打印結(jié)果都是1款熬,即一個(gè)代碼單元(UTF-16使用2個(gè)字節(jié)存儲(chǔ)英文和常見(jiàn)中文)深寥。
String.toCharArray().length返回的是:JVM中字符串對(duì)應(yīng)的字符數(shù)組的數(shù)組長(zhǎng)度。由于1個(gè)char便能表示英文和常見(jiàn)中文贤牛。因此對(duì)于(4)和(7)惋鹅,無(wú)論文件使用UTF8還是UTF16進(jìn)行編碼存儲(chǔ),在JVM中均使用UTF16進(jìn)行存儲(chǔ)殉簸,所以返回的數(shù)組長(zhǎng)度均是1
String.getBytes()方法返回的是:字符串在數(shù)據(jù)存儲(chǔ)層面所占的字節(jié)數(shù)闰集。注意:當(dāng)getBytes()方法不含參數(shù)時(shí),則表明遵循文件所采用的編碼規(guī)則般卑。
對(duì)于(5)來(lái)說(shuō)武鲁,如果文件采用UTF8進(jìn)行編碼存儲(chǔ),則返回1蝠检,即UTF8使用1個(gè)字節(jié)存儲(chǔ)英文沐鼠。如果采用UTF16進(jìn)行編碼,則返回4,原因是UTF16采用2個(gè)字節(jié)存儲(chǔ)英文饲梭,另外2個(gè)字節(jié)用于存儲(chǔ)BOM乘盖。對(duì)于(8)來(lái)說(shuō),如果文件采用UTF8進(jìn)行編碼存儲(chǔ)憔涉,則返回3订框,即UTF8使用3個(gè)字節(jié)存儲(chǔ)中文。如果采用UTF16進(jìn)行編碼兜叨,則返回4布蔗,原因是UTF16采用2個(gè)字節(jié)存儲(chǔ)中文,另外2個(gè)字節(jié)用于存儲(chǔ)BOM浪腐。
亂碼的本質(zhì)
亂碼的本質(zhì)就是:編碼與解碼所采用的編碼規(guī)則不一致
如果你未能清晰的理解什么是編碼,什么是解碼顿乒,請(qǐng)看下面這張神圖:
從右到左的這個(gè)過(guò)程就是編碼议街,從左到右的這個(gè)過(guò)程就是解碼。下面通過(guò)兩個(gè)例子進(jìn)一步說(shuō)明璧榄。
編碼與解碼的例子
//類文件本身采用UTF-8格式
String str1 = "中"; //編碼
System.out.println(str1); //解碼
第一行代碼表示編碼的過(guò)程:
- 把"中"字讀到JVM內(nèi)存中特漩,在JVM中,String類型使用UTF-16進(jìn)行編碼骨杂。
- 由于文件本身采用UTF-8格式涂身,因此JVM將負(fù)責(zé)把UTF-16轉(zhuǎn)為UTF-8。
- 由于UTF-8遵循Unicode字符集搓蚪,因此再進(jìn)一步把Unicode碼點(diǎn)按照UTF-8的要求進(jìn)行編碼蛤售。(黃色階段)
- 最后把二進(jìn)制數(shù)據(jù)保存到文件中。
第二行代碼則表示解碼的過(guò)程:
- 把文件中的二進(jìn)制數(shù)據(jù)讀取出來(lái)妒潭。
- 由于文件是UTF-8格式悴能,因此采用UTF-8對(duì)二進(jìn)制數(shù)據(jù)進(jìn)行解碼,并得到Unicode碼點(diǎn)雳灾。(黃色階段)
- 由于JVM使用UTF-16格式漠酿,因此數(shù)據(jù)讀到JVM后,JVM負(fù)責(zé)轉(zhuǎn)譯谎亩。
- 轉(zhuǎn)譯后炒嘲,UTF-16格式的的"中"字被打印到控制臺(tái)。
在上面的兩行代碼中匈庭,由于編碼階段與解碼階段所采用的編碼規(guī)則都是一致的夫凸,所以肯定不會(huì)造成亂碼。
亂碼的例子
String str2 = new String("中".getBytes(), "GBK"); //(1) getBytes()是編碼嚎花, GBK是解碼
System.out.println(str2.toCharArray().length); //lenght=2, 亂碼
/*
亂碼:UTF8使用3字節(jié)存儲(chǔ)一個(gè)中文寸痢,而GBK使用2字節(jié)。
因此GBK把前兩個(gè)字節(jié)作為一個(gè)中文紊选,最后一個(gè)字節(jié)作為另一個(gè)中文啼止。
但由于unicode與GBK的中文碼點(diǎn)不一樣道逗,因此造成亂碼
*/
for(int i=0; i<str2.toCharArray().length; i++) {
System.out.print(str2.toCharArray()[i]);
}
/*
由于str2已被解析為2個(gè)亂碼中文字,因此對(duì)于UTF8的存儲(chǔ)格式献烦,需要6個(gè)字節(jié)存儲(chǔ)兩個(gè)中文字
*/
System.out.println(str2.getBytes().length); //length=6
在上面的例子中滓窍,str2的值其實(shí)是一個(gè)亂碼。原因是在執(zhí)行第(1)行代碼時(shí)巩那,經(jīng)歷了編碼和解碼兩個(gè)過(guò)程:
- 執(zhí)行"中".getBytes();時(shí)吏夯,返回的是"中"字按照文件的編碼格式(即UTF-8)進(jìn)行編碼后的二進(jìn)制數(shù)據(jù)。
- new String(XXX, "GBK",)即横;的構(gòu)造函數(shù)則是表示對(duì)上一步獲得的二進(jìn)制數(shù)據(jù)噪生,按照GBK編碼規(guī)則進(jìn)行解碼,已得到對(duì)應(yīng)的字符串东囚。
到這里你應(yīng)該就明白了跺嗽,第一步使用的是UTF-8進(jìn)行編碼,第二步則使用了GBK進(jìn)行解碼页藻,肯定會(huì)造成亂碼啦桨嫁!
常見(jiàn)疑問(wèn)
如何確定不同編碼規(guī)則的兼容性?
可以通過(guò)下面幾個(gè)實(shí)驗(yàn)來(lái)確定(這里主要是說(shuō)兼容英文和數(shù)字):
實(shí)驗(yàn)一:
- 在MyEclipse中創(chuàng)建一個(gè)類份帐,右鍵-->properties璃吧,設(shè)置編碼格式為UTF-8(假設(shè)這個(gè)文件不含中文)。
- 把這個(gè)文件修改為ASCII废境, ISO8859-1畜挨, GB2312, 你會(huì)發(fā)現(xiàn)該文件均不會(huì)造成亂碼彬坏。也就是說(shuō)對(duì)于英文和數(shù)字朦促,UTF-8與ASCII,ISO8859-1栓始,GB2312是兼容的务冕。
- 如果把這個(gè)文件從UTF-8修改為UTF-16格式,你會(huì)發(fā)現(xiàn)文件亂碼幻赚。也就是說(shuō)禀忆,UTF-8與UTF-16是不兼容的。
實(shí)驗(yàn)二:
假設(shè)實(shí)驗(yàn)一中的類是UTF-16格式的(文件不含中文)落恼,修改為UTF-8箩退,ASCII,ISO8859-1佳谦,GB2312戴涝,你會(huì)發(fā)現(xiàn)文件都會(huì)造成亂碼。也就是會(huì)所,UTF-16與UTF-8啥刻,ASCII奸鸯,ISO8859-1,GB2312都不兼容可帽。
UTF-8和UTF-16到底是什么關(guān)系娄涩?
UTF-8與UTF-16均支持Unicode字符集。所以對(duì)于同一個(gè)字符來(lái)說(shuō)映跟,使用UTF-8編碼與UTF-16編碼的碼點(diǎn)都是一樣的蓄拣。例如“中”字均使用20013作為碼點(diǎn)。但是在數(shù)據(jù)存儲(chǔ)層面努隙,UTF-8與UTF-16是不一樣的球恤。例如,我們可以直接把20013轉(zhuǎn)為二進(jìn)制數(shù)字進(jìn)行存儲(chǔ)荸镊,也可以在20013后面加上后綴000碎捺,然后再轉(zhuǎn)為二進(jìn)制進(jìn)行存儲(chǔ)。從這個(gè)例子中便可以知道贷洲,如果不同的編碼規(guī)則實(shí)現(xiàn)了同一個(gè)字符集,那么該字符的碼點(diǎn)應(yīng)該是相同的晋柱,但是在數(shù)據(jù)存儲(chǔ)方面卻不一定相同优构。具體的存儲(chǔ)細(xì)節(jié)詳見(jiàn)References的第一篇。
我應(yīng)該使用哪種編碼規(guī)則雁竞?
建議首選UTF-8钦椭。原因如下:
- 因?yàn)閁TF-8支持所有自然語(yǔ)言,
- 在存儲(chǔ)空間方面比UTF-16更具有優(yōu)勢(shì)碑诉。
- 兼容ASCII彪腔,ISO8859-1,方便轉(zhuǎn)換进栽。
什么時(shí)候使用UTF-16?
答:建議永遠(yuǎn)不要選擇UTF-16!因?yàn)閁TF-16含有BOM邻吭,BOM非衬蟠疲坑爹的東西,一不小心就會(huì)造成各種錯(cuò)誤唠帝。而且UTF-8完全可以代替UTF-16屯掖。
為什么使用UTF-16存儲(chǔ)一個(gè)常見(jiàn)中文字符卻占4個(gè)字節(jié)?
答:UTF-16存儲(chǔ)一個(gè)常見(jiàn)中文字符需要2個(gè)字節(jié)襟衰,但是UTF-16本身含有BOM贴铜,BOM也需要占據(jù)2個(gè)字節(jié)。
在哪些地方需要設(shè)置編碼規(guī)則?
- 對(duì)于一個(gè)myeclipse項(xiàng)目中绍坝,所有的.java, .jsp, .txt, .xml, .js, .css等常見(jiàn)文件徘意,最好都設(shè)置為UTF-8編碼規(guī)則。
- 把tomcat等中間件設(shè)置為UTF-8編碼規(guī)則陷嘴。
- 把數(shù)據(jù)庫(kù)設(shè)置為UTF-8編碼規(guī)則映砖。
- 對(duì)于接收的參數(shù),比如前端傳過(guò)來(lái)的參數(shù)灾挨,均使用UTF-8進(jìn)行編碼和解碼邑退。
char類型對(duì)中文的支持
Java中的char類型使用2個(gè)字節(jié)表示中文,英文字符劳澄〉丶迹可能有人會(huì)問(wèn):對(duì)于一個(gè)使用UTF-8編碼的類,它使用3個(gè)字節(jié)存儲(chǔ)中文秒拔,但是我們依然可以在這個(gè)類里面莫矗,把中文字符保存到2個(gè)字節(jié)長(zhǎng)度的char類型中,這是為什么呢砂缩?原因是:在數(shù)據(jù)存儲(chǔ)層面作谚,一個(gè)中文字符確實(shí)是按照UTF-8的規(guī)定,以3個(gè)字節(jié)的方式保存在文件中庵芭。但是當(dāng)中文字符被讀到JVM內(nèi)存中妹懒,該字符會(huì)被轉(zhuǎn)為UTF-16,并以2個(gè)字節(jié)的方式保存在JVM內(nèi)存中双吆。簡(jiǎn)單來(lái)說(shuō)就是:在UTF-8文件中眨唬,中文字符以UTF-8進(jìn)行存儲(chǔ),但是讀到JVM內(nèi)存中時(shí)好乐,會(huì)轉(zhuǎn)換成UTF-16進(jìn)行存儲(chǔ)匾竿。另外還需要注意的是,由于char的長(zhǎng)度是2個(gè)字節(jié)蔚万,因此char類型無(wú)法表示罕見(jiàn)中文字符岭妖。