[字符集與編碼 2] 編碼 解碼 亂碼

編碼規(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); //解碼

第一行代碼表示編碼的過程:

  1. 把"中"字讀到JVM內存中,在JVM中,String類型使用UTF-16進行編碼。
  2. 由于文件本身采用UTF-8格式,因此JVM將負責把UTF-16轉為UTF-8。
  3. 由于UTF-8遵循Unicode字符集,因此再進一步把Unicode碼點按照UTF-8的要求進行編碼。(黃色階段)
  4. 最后把二進制數(shù)據(jù)保存到文件中。

第二行代碼則表示解碼的過程:

  1. 把文件中的二進制數(shù)據(jù)讀取出來。
  2. 由于文件是UTF-8格式,因此采用UTF-8對二進制數(shù)據(jù)進行解碼,并得到Unicode碼點。(黃色階段)
  3. 由于JVM使用UTF-16格式,因此數(shù)據(jù)讀到JVM后,JVM負責轉譯。
  4. 轉譯后,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)歷了編碼和解碼兩個過程:

  1. 執(zhí)行"中".getBytes();時,返回的是"中"字按照文件的編碼格式(即UTF-8)進行編碼后的二進制數(shù)據(jù)。
  2. new String(XXX, "GBK",);的構造函數(shù)則是表示對上一步獲得的二進制數(shù)據(jù),按照GBK編碼規(guī)則進行解碼,已得到對應的字符串。

到這里你應該就明白了,第一步使用的是UTF-8進行編碼,第二步則使用了GBK進行解碼,肯定會造成亂碼啦!

常見疑問

如何確定不同編碼規(guī)則的兼容性?

可以通過下面幾個實驗來確定(這里主要是說兼容英文和數(shù)字):
實驗一:

  1. 在MyEclipse中創(chuàng)建一個類,右鍵-->properties,設置編碼格式為UTF-8(假設這個文件不含中文)。
  2. 把這個文件修改為ASCII, ISO8859-1, GB2312, 你會發(fā)現(xiàn)該文件均不會造成亂碼。也就是說對于英文和數(shù)字,UTF-8與ASCII,ISO8859-1,GB2312是兼容的。
  3. 如果把這個文件從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ī)則?

  1. 對于一個myeclipse項目中,所有的.java, .jsp, .txt, .xml, .js, .css等常見文件,最好都設置為UTF-8編碼規(guī)則。
  2. 把tomcat等中間件設置為UTF-8編碼規(guī)則。
  3. 把數(shù)據(jù)庫設置為UTF-8編碼規(guī)則。
  4. 對于接收的參數(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類型無法表示罕見中文字符。

References

  1. http://my.oschina.net/goldenshaw/blog?catalog=536953
  2. http://blog.xieyc.com/common-code-standard-unicode-utf-iso-8859-1-etc/
  3. http://www.regexlab.com/zh/encoding.htm
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容