編碼規(guī)則
如果你已經(jīng)閱讀了JavaHipster 1中references提到的兩篇文章,你應該明白:從字符集到編碼規(guī)則的過程,實際上就是從字符集-->到編號-->到編碼的過程。
關于編碼規(guī)則,主要關注兩個方面:
- 所遵循的字符集
- 存儲方式:使用幾個字節(jié)來存儲字符
ASCII編碼規(guī)則
- 遵循ASCII字符集,共含有255個字符(有很多字符是保留字符)
- 使用1個字節(jié)存儲(定長)
ISO8859-1編碼規(guī)則
- 遵循ISO8859-1字符集,共含有255個字符。
- 使用1個字節(jié)存儲(定長)
- ISO8859-1兼容ASCII,也就是說,對于同樣的字符,ASCII與ISO8859-1對應的編號(也成為code point碼點)都是一樣的。
GB2312編碼規(guī)則
- 遵循GB2312字符集,支持常見的中文。
- 使用1個字節(jié)存儲英文和數(shù)字等字符,使用2個字節(jié)存儲中文字符。
- GB2312兼容ASCII。
GBK編碼規(guī)則
- 遵循GBK字符集,支持常見的中文,罕見中文,繁體中文,日文的假名。
- 使用1個字節(jié)存儲英文和數(shù)字等字符,使用2個字節(jié)存儲中文字符。
- GBK兼容ASCII與GB2312。
UTF-16編碼規(guī)則
- 遵循Unicode字符集。支持地球上所有常見的自然語言。
- 使用2個字節(jié)存儲各種語言的常用字符,使用4個字節(jié)存儲其他罕見字符。
- UTF-16并不兼容ASCII或者ISO8859-1,原因是UTF-16使用2個字節(jié)表示英文和西歐字符。
- 使用UTF-16編碼的文件,在文件頭部均含有BOM,以說明這個文件使用大端法還是小端法進行存儲。
- 在有一些文本編輯器中(比如notepad++),會把UTF-16稱為UCS-2。
UTF-8編碼規(guī)則
- 遵循Unicode字符集。支持地球上所有常見的自然語言。
- 使用1個字節(jié)存儲英文和數(shù)字等字符,使用3個字節(jié)存儲常用中文,使用4個字符存儲罕見字符。
- UTF-8兼容ASCII,但不兼容UTF-16,因為UTF-8存儲字符所需的空間與UTF-16是不一樣的。
- UTF-8默認不帶BOM。最好也不要加上BOM。
ANSI
ANSI嚴格來說并不是一種編碼規(guī)則,它表示:根據(jù)當前操作系統(tǒng)以及操作系統(tǒng)的語言,選擇對應的編碼規(guī)則進行編碼。例如,對于簡體中文的Windows操作系統(tǒng),ANSI代表GBK。在繁體中文Windows操作系統(tǒng)中,ANSI代表Big5。在日文Windows操作系統(tǒng)中,ANSI代表Shift_JIS 編碼。
文件編碼規(guī)則與JVM編碼規(guī)則
對于一個文件而言,我們可以顯式的指定文件的編碼規(guī)則。但當文件中的內容讀到JVM內存中后,將會采用JVM的編碼規(guī)則進行重新編碼和存儲。例如,在JVM中,Java的char類型和String類型均強制使用UTF-16進行編碼。因此對于一個非UTF-16的文件來說,在數(shù)據(jù)存儲層面,數(shù)據(jù)依舊采用文件本身的編碼規(guī)則進行編碼和存儲。但當數(shù)據(jù)讀到JVM中,將采用UTF-16進行編碼和存儲。
下面的例子將解釋:文件編碼規(guī)則與JVM編碼規(guī)則的區(qū)別
//把下面的代碼分別在UTF-8與UTF-16編碼格式的文件下進行測試
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。對于(1)和(2)而言,在UTF8和UTF16環(huán)境下,打印結果均是16bit,即兩個字符。原因是:無論文件采用哪種編碼規(guī)則中,在JVM中,char類型均使用2個字節(jié)存儲字符。
String.length()返回的是JVM中字符串所對應的代碼單元長度。在Java中,字符串是由char字符數(shù)組所組成的,因此在JVM中字符串也使用UTF16進行存儲。
代碼單元指一種轉換格式中最小的一個分隔,由于UTF16最少使用2個字節(jié)表示一個字符,因此對于UTF-16而言,1個代碼單元等價于2個字節(jié)。而對于UTF-8而言,1個代碼單元等價于1個字節(jié)。
對于(3)和(6)而言,無論文件使用UTF8還是UTF16進行編碼存儲,在JVM中,String均使用UTF-16進行存儲,因此打印結果都是1,即一個代碼單元(UTF-16使用2個字節(jié)存儲英文和常見中文)。
String.toCharArray().length返回的是:JVM中字符串對應的字符數(shù)組的數(shù)組長度。由于1個char便能表示英文和常見中文。因此對于(4)和(7),無論文件使用UTF8還是UTF16進行編碼存儲,在JVM中均使用UTF16進行存儲,所以返回的數(shù)組長度均是1
String.getBytes()方法返回的是:字符串在數(shù)據(jù)存儲層面所占的字節(jié)數(shù)。注意:當getBytes()方法不含參數(shù)時,則表明遵循文件所采用的編碼規(guī)則。
對于(5)來說,如果文件采用UTF8進行編碼存儲,則返回1,即UTF8使用1個字節(jié)存儲英文。如果采用UTF16進行編碼,則返回4,原因是UTF16采用2個字節(jié)存儲英文,另外2個字節(jié)用于存儲BOM。對于(8)來說,如果文件采用UTF8進行編碼存儲,則返回3,即UTF8使用3個字節(jié)存儲中文。如果采用UTF16進行編碼,則返回4,原因是UTF16采用2個字節(jié)存儲中文,另外2個字節(jié)用于存儲BOM。
亂碼的本質
亂碼的本質就是:編碼與解碼所采用的編碼規(guī)則不一致
如果你未能清晰的理解什么是編碼,什么是解碼,請看下面這張神圖:
從右到左的這個過程就是編碼,從左到右的這個過程就是解碼。下面通過兩個例子進一步說明。
編碼與解碼的例子
//類文件本身采用UTF-8格式
String str1 = "中"; //編碼
System.out.println(str1); //解碼
第一行代碼表示編碼的過程:
- 把"中"字讀到JVM內存中,在JVM中,String類型使用UTF-16進行編碼。
- 由于文件本身采用UTF-8格式,因此JVM將負責把UTF-16轉為UTF-8。
- 由于UTF-8遵循Unicode字符集,因此再進一步把Unicode碼點按照UTF-8的要求進行編碼。(黃色階段)
- 最后把二進制數(shù)據(jù)保存到文件中。
第二行代碼則表示解碼的過程:
- 把文件中的二進制數(shù)據(jù)讀取出來。
- 由于文件是UTF-8格式,因此采用UTF-8對二進制數(shù)據(jù)進行解碼,并得到Unicode碼點。(黃色階段)
- 由于JVM使用UTF-16格式,因此數(shù)據(jù)讀到JVM后,JVM負責轉譯。
- 轉譯后,UTF-16格式的的"中"字被打印到控制臺。
在上面的兩行代碼中,由于編碼階段與解碼階段所采用的編碼規(guī)則都是一致的,所以肯定不會造成亂碼。
亂碼的例子
String str2 = new String("中".getBytes(), "GBK"); //(1) getBytes()是編碼, GBK是解碼
System.out.println(str2.toCharArray().length); //lenght=2, 亂碼
/*
亂碼:UTF8使用3字節(jié)存儲一個中文,而GBK使用2字節(jié)。
因此GBK把前兩個字節(jié)作為一個中文,最后一個字節(jié)作為另一個中文。
但由于unicode與GBK的中文碼點不一樣,因此造成亂碼
*/
for(int i=0; i<str2.toCharArray().length; i++) {
System.out.print(str2.toCharArray()[i]);
}
/*
由于str2已被解析為2個亂碼中文字,因此對于UTF8的存儲格式,需要6個字節(jié)存儲兩個中文字
*/
System.out.println(str2.getBytes().length); //length=6
在上面的例子中,str2的值其實是一個亂碼。原因是在執(zhí)行第(1)行代碼時,經(jīng)歷了編碼和解碼兩個過程:
- 執(zhí)行"中".getBytes();時,返回的是"中"字按照文件的編碼格式(即UTF-8)進行編碼后的二進制數(shù)據(jù)。
- new String(XXX, "GBK",);的構造函數(shù)則是表示對上一步獲得的二進制數(shù)據(jù),按照GBK編碼規(guī)則進行解碼,已得到對應的字符串。
到這里你應該就明白了,第一步使用的是UTF-8進行編碼,第二步則使用了GBK進行解碼,肯定會造成亂碼啦!
常見疑問
如何確定不同編碼規(guī)則的兼容性?
可以通過下面幾個實驗來確定(這里主要是說兼容英文和數(shù)字):
實驗一:
- 在MyEclipse中創(chuàng)建一個類,右鍵-->properties,設置編碼格式為UTF-8(假設這個文件不含中文)。
- 把這個文件修改為ASCII, ISO8859-1, GB2312, 你會發(fā)現(xiàn)該文件均不會造成亂碼。也就是說對于英文和數(shù)字,UTF-8與ASCII,ISO8859-1,GB2312是兼容的。
- 如果把這個文件從UTF-8修改為UTF-16格式,你會發(fā)現(xiàn)文件亂碼。也就是說,UTF-8與UTF-16是不兼容的。
實驗二:
假設實驗一中的類是UTF-16格式的(文件不含中文),修改為UTF-8,ASCII,ISO8859-1,GB2312,你會發(fā)現(xiàn)文件都會造成亂碼。也就是會所,UTF-16與UTF-8,ASCII,ISO8859-1,GB2312都不兼容。
UTF-8和UTF-16到底是什么關系?
UTF-8與UTF-16均支持Unicode字符集。所以對于同一個字符來說,使用UTF-8編碼與UTF-16編碼的碼點都是一樣的。例如“中”字均使用20013作為碼點。但是在數(shù)據(jù)存儲層面,UTF-8與UTF-16是不一樣的。例如,我們可以直接把20013轉為二進制數(shù)字進行存儲,也可以在20013后面加上后綴000,然后再轉為二進制進行存儲。從這個例子中便可以知道,如果不同的編碼規(guī)則實現(xiàn)了同一個字符集,那么該字符的碼點應該是相同的,但是在數(shù)據(jù)存儲方面卻不一定相同。具體的存儲細節(jié)詳見References的第一篇。
我應該使用哪種編碼規(guī)則?
建議首選UTF-8。原因如下:
- 因為UTF-8支持所有自然語言,
- 在存儲空間方面比UTF-16更具有優(yōu)勢。
- 兼容ASCII,ISO8859-1,方便轉換。
什么時候使用UTF-16?
答:建議永遠不要選擇UTF-16!因為UTF-16含有BOM,BOM非??拥臇|西,一不小心就會造成各種錯誤。而且UTF-8完全可以代替UTF-16。
為什么使用UTF-16存儲一個常見中文字符卻占4個字節(jié)?
答:UTF-16存儲一個常見中文字符需要2個字節(jié),但是UTF-16本身含有BOM,BOM也需要占據(jù)2個字節(jié)。
在哪些地方需要設置編碼規(guī)則?
- 對于一個myeclipse項目中,所有的.java, .jsp, .txt, .xml, .js, .css等常見文件,最好都設置為UTF-8編碼規(guī)則。
- 把tomcat等中間件設置為UTF-8編碼規(guī)則。
- 把數(shù)據(jù)庫設置為UTF-8編碼規(guī)則。
- 對于接收的參數(shù),比如前端傳過來的參數(shù),均使用UTF-8進行編碼和解碼。
char類型對中文的支持
Java中的char類型使用2個字節(jié)表示中文,英文字符??赡苡腥藭枺簩τ谝粋€使用UTF-8編碼的類,它使用3個字節(jié)存儲中文,但是我們依然可以在這個類里面,把中文字符保存到2個字節(jié)長度的char類型中,這是為什么呢?原因是:在數(shù)據(jù)存儲層面,一個中文字符確實是按照UTF-8的規(guī)定,以3個字節(jié)的方式保存在文件中。但是當中文字符被讀到JVM內存中,該字符會被轉為UTF-16,并以2個字節(jié)的方式保存在JVM內存中。簡單來說就是:在UTF-8文件中,中文字符以UTF-8進行存儲,但是讀到JVM內存中時,會轉換成UTF-16進行存儲。另外還需要注意的是,由于char的長度是2個字節(jié),因此char類型無法表示罕見中文字符。