編碼字符集與Java -Java World亂碼問題根源之所在。
阿新 • • 發佈:2019-01-11
本文介紹了編碼字符集的概念以及Java與編碼字符集之間的關係,文章的內容來自於本人工作過程中的經驗積累以及網路中的相關文章介紹,如果文章中有任何紕漏歡迎讀者指正,讓我們共同討論學習J
1.字元
字元是抽象的最小文字單位。它沒有固定的形狀(可能是一個字形),而且沒有值。“A”是一個字元,“€”(德國、法國和許多其他歐洲國家通用貨幣的標誌)也是一個字元。“中”“國”這是兩個漢字字元。字元僅僅代表一個符號,沒有任何實際值的意義。
2.字符集 字符集是字元的集合。例如,漢字字元是中國人最先發明的字元,在中文、日文、韓文和越南文的書寫中使用。這也說明了字元和字符集之間的關係,字元組成字符集。
3.編碼字符集 編碼字符集是一個字符集(有時候也被簡稱位字符集),它為每一個字元分配一個唯一數字。最早的編碼是iso8859-1,和ascii編碼相似。但為了方便表示各種各樣的語言,逐漸出現了很多標準編碼。 iso8859-1: 屬於單位元組編碼字符集,最多能表示的字元範圍是0-255,應用於英文系列,除了iso8859-1以外還有其他iso8859系列的編碼,這些編碼都是為了滿足歐洲國家語言字元的需要而設計的。
GB2312/GBK/ GB18030:前面提到的iso8859-1最多隻能表示256個字元,這對於漢字來說實在是有些抱歉,所以就有了現在要介紹的漢字國標碼,專門用來表示漢字,是雙位元組編碼字符集,而英文字母和iso8859-1一致(相容iso8859-1編碼)。其中GBK編碼能夠用來同時表示繁體字和簡體字,而GB2312只能表示簡體字,GBK是相容GB2312編碼的。而GB18030-2000則是一個更復雜的字符集,採用變長位元組的編碼方式,能夠支援更多的字元。需要注意的是中國政府要求所有在中國出售的軟體必須支援 GB18030。
Unicode:這是最統一的編碼字符集,可以用來表示所有語言的字元,不相容任何前面提到的編碼字符集。Unicode 標準始終使用十六進位制數字,而且在書寫時在前面加上字首“U+”,所以“A”的編碼書寫為“U+0041”。注意:在JAVA語言中書寫時應該使用轉義符‘/u’表示,如 char charA = ‘/u0041’; 這種表示方法等與 char charA = ‘A’; 。
從ASCII(英文) ==> 西歐文字 ==> 東歐字符集(俄文,希臘語等) ==> 東亞字符集(GB2312 BIG5 SJIS等)==> 擴充套件字符集GBK GB18030這個發展過程基本上也反映了字符集標準的發展過程,但這麼隨著時間的推移,尤其是網際網路讓跨語言的資訊的互動變得越來越多的時候,太多多針對本地語言的編碼標準的出現導致一個應用程式的國際化變得成本非常高。尤其是你要編寫一個同時包含法文和簡體中文的文件,這時候一般都會想到要是用一個通用的字符集能夠顯示所有語言的所有文字就好了,而且這樣做應用也能夠比較方便的國際化,為了達到這個目標,即使應用犧牲一些空間和程式效率也是非常值得的。 UNICODE就是這樣一個通用的解決方案。
4.Unicode編碼字符集
Unicode 因為必須將中、韓、日、英、法、阿拉伯……等許多國家所使用的文字都納入,目前已經包含了六萬多個字元,所以 Unicode 使用了16個位來為字元編碼。因為 Unicode 使用了 16 位編碼,所以每個字元都用 16 位來儲存或傳輸是很自然的事,這種儲存或傳輸的格式稱為UTF-16(一種Unicode的字元編碼方案,在這裡所說的UTF-16並不涉及增補字元的表示,本文將會在稍後介紹)。但是如果你使用到的字元都是西方字元,那麼你一定不會想用 UTF-16 的格式,因為體積比8位的iso8859-1多了一倍,如此一來就必須考慮程式執行時各種字元在記憶體中所佔空間的效能問題,這便引入了字元編碼方案的概念:
字元編碼方案是從一個或多個編碼字符集到一個或多個固定寬度程式碼單元序列的對映。
最常用的程式碼單元是位元組,所以可以簡單的認為字元編碼方案是為了告訴計算機如何將編碼字符集(如Unicode)對映到計算機可以識別的資料格式中,如位元組。這種編碼方案往往能夠為他所對應的字符集在計算機處理時提供更為優化的空間以及效能上的解決方案。Unicode編碼字符集有三種字元編碼方案,下面將逐一介紹: lUTF-32* 即將每一個Unicode編碼表示為相同值的32位整數。很明顯,它是內部處理最方便的表達方式,但是,如果作為一般字串表達方式,則要消耗更多的記憶體。顯而易見,對於英文字母的表示將需要多個0位元組,僅僅因為我們需要4個位元組32位來表示一個Unicode字元。 lUTF-16 使用一個或兩個未分配的 16位程式碼單元的序列對Unicode編碼進行編碼。值U+0000至U+FFFF 編碼為一個相同值的16位單元。增補字元*編碼為兩個程式碼單元,第一個單元來自於高代理範圍(U+D800 至 U+DBFF),第二個單元來自於低代理範圍(U+DC00 至U+DFFF)。這在概念上可能看起來類似於多位元組編碼,但是其中有一個重要區別:值 U+D800 至 U+DFFF 保留用於 UTF-16;沒有這些值分配字元作為程式碼點。這意味著,對於一個字串中的每個單獨的程式碼單元,軟體可以識別是否該程式碼單元表示某個單單元字元,或者是否該程式碼單元是某個雙單元字元的第一個或第二單元。這相當於某些傳統的多位元組字元編碼來說是一個顯著的改進,在傳統的多位元組字元編碼中,位元組值0x41 既可能表示字母“A”,也可能是一個雙位元組字元的第二個位元組。 lUTF-8 使用一至四個位元組的序列對編碼Unicode進行編碼。U+0000至U+007F使用一個位元組編碼,U+0080至U+07FF 使用兩個位元組,U+0800 至U+FFFF使用三個位元組,而U+10000至U+10FFFF使用四個位元組。UTF-8設計原理為:位元組值0x00至0x7F始終表示程式碼點U+0000至U+007F(Basic Latin 字元子集,它對應 ASCII 字符集)。這些位元組值永遠不會表示其他Unicode編碼字元,這一特性使 UTF-8 可以很方便地在軟體中將特殊的含義賦予某些 ASCII 字元。UTF-8 的格式在編碼英文時,只需要8位,但是中文則是24位,其他更加偏僻的字元才又可能是32位,這也是UTF-8最大的編碼特點,可以最高效率的利用計算機空間,因為在計算機處理的時候大多數情況下還是隻使用英文進行運算和處理,這也是為什麼還需要UTF-8的主要原因,因為畢竟網際網路70%以上的資訊仍然是英文。如果連英文都用2個位元組存取(UCS-2),空間浪費不就太多了? * UTF-32 表示Unicode Transformation Form 32-bit form,UTF-16,UTF-8依此類推。 * Unicode 最初設計是作為一種固定寬度的 16 位字元編碼。在 Java 程式語言中,基本資料型別 char 初衷是通過提供一種簡單的、能夠包含任何字元的資料型別來充分利用這種設計的優點。不過,現在看來,16 位編碼的所有65,536個字元並不能完全表示全世界所有正在使用或曾經使用的字元。於是,Unicode 標準已擴充套件到包含多達 1,112,064個字元。那些超出原來的16位限制的字元被稱作增補字元。
5.Java與編碼字符集 從上面的介紹我們知道了Unicode編碼字符集可以用來表示世界上所有的語言文字。Java內部處理字元使用的字序方式是Unicode,這是一種通行全球的編碼方式,他使Java語言能夠描述世界上所有的文字。在Java程式中對各種字元在記憶體中處理是使用Unicode的UTF-8編碼方式,這也是因為UTF-8的特點所決定的,Class File(也就是bytecode)中有一欄位叫做常數區(Constant Pool),一律使用UTF-8為子元編碼。 這看起來一切正常,Java可以處理世界上所有的字元,一切都是按照秩序在執行,但是,從前面的討論我們知道,世界上並不是僅僅只有Unicode編碼字符集,同時存在的還有iso8859-1、GBK等編碼字符集,就是在Unicode中也同樣存在著UTF-8,UTF-16,UTF-32等多種編碼,如果傳入的位元組編碼採用的是GB18030,而採用的解碼方式為UTF-8那會有什麼後果呢,看看下面的程式碼片段: public static final String TEST_RESOURCE = "你好"; public static void testEncoding() { try { byte[] bytes = TEST_RESOURCE.getBytes("GB18030"); String result = new String(bytes, "UTF-8"); System.out.println("Receive value: [" + result + "]."); } catch (UnsupportedEncodingException e) { // TODO Auto-generated catch block e.printStackTrace(); } } 執行以上的程式碼片段,在我的機器(Win XP中文版)上面得到的結果是: Receive value: [���]. 明白了吧,這就是久負盛名的亂碼問題的根源,目前在市面上存在有多種編碼字符集,以及編碼字符集的編碼方案,所以雖然在Java中內部是以Unicode的UTF-8來處理各種字元的表示以及運算,但是這僅僅是在Java內部而以,如果Java程式需要和外部應用系統進行互動,比如與作業系統,資料庫系統之間的互動,那麼在這些互動過程中如何處理字符集的編碼解碼是解決好Java應用程式亂碼問題的根源。 如果將上面的程式碼塊修改成如下的程式碼塊: public static void testEncoding() { try { byte[] bytes = TEST_RESOURCE.getBytes("GB18030"); String result = new String(bytes, "GB18030"); System.out.println("Receive value: [" + result + "]."); } catch (UnsupportedEncodingException e) { // TODO Auto-generated catch block e.printStackTrace(); } } 注意紅色標註的地方,執行以上的程式碼塊將會受到預期的結果: Receive value: [你好]. 統一字元的編碼型別和解碼型別,如此一來任何亂碼問題都不會再是問題了。在網上可以搜尋到N多的關於如何解決J2EE亂碼問題的文章,我在這裡也就不廢話了,我只是想說說Java亂碼問題的根源之所在。 如果你仔細想想Java的開發過程,原檔案編寫、javac編譯、java執行,這每一步驟都會涉及到編碼的轉換過程,這個過程總是存在的,只是有的時候用預設的引數進行。 我們從javac這個命令來開始我們的分析,編譯的時候,如果你不說明原始檔編碼方式的話,javac 編譯器在讀進此原始程式檔案開始編譯之前,會先去詢問作業系統檔案預設的編碼方式為何。以我的作業系統WIN XP 中文版來說,javac 會先詢問WIN XP,得知當前的編碼是用GB18030的方式編碼。然後就可以將原始檔由GB18030轉成Unicode編碼方式,開始進行編譯。在這裡就會發生一下一些編碼問題: l如果作業系統的國籍資料設定錯誤,會造成javac編譯器取得的編碼資訊是錯誤的,這裡也有可能由於系統屬性file.encoding設定錯誤,在我的系統中該屬性為GB18030,可以通過程式碼System.out.println(System.getProperties());輸出可能的系統屬性。 l較差勁的編譯器可能沒有主動詢問作業系統的編碼方式,而是採用編譯器預設的編碼方式,當然這種情況對於目前先進的編譯器來說已經不存在了,但是這確實是一種可能的原因。 l原始碼是在英文作業系統上書寫採用編碼iso8859-1,寫好以後再將原始碼傳遞給中文作業系統進行編譯,這樣由於兩個作業系統的編碼方式不同,也會造成javac執行錯誤。 明白了吧,這些問題在我們日常的程式碼編寫過程中,往往由於預設的屬性都正好能滿足我們的需要,即原始碼的書寫以及編譯都採用作業系統預設的編碼方式,所以可能很多人到目前為止都沒有遇見過諸如此類的問題,但是我們要知道,這些問題確實是存在的。 Java編譯器在執行過程中給我們提供了可選的encoding引數來告訴編譯器該採用何種編碼方式將讀入的原始檔轉換成Unicode編碼方式,然後再進行後續的編譯工作。 javac –encoding GB18030 …. 6.Inside OK,通過前面的介紹希望讀者能夠對Java以及各種字元編碼之間的關係有個簡單的瞭解,下面我在繼續總結一下: lJavac是以系統預設編碼(file.encoding系統屬性)讀入原始檔,然後按Unicode進行編碼的。 l在JAVA執行的時候,JAVA也是採用Unicode編碼的,為了高度利用記憶體空間提高效率對Unicode字元編碼採用了UTF-8的方式編碼,並且預設輸入和輸出的都是作業系統的預設編碼。 l也就是說在new String(bytes,encode)中,系統認為輸入的是編碼為encode的位元組流,換句話說,如果按encode來翻譯bytes才能得到正確的結果;而在new String(bytes)中採用的就是根據file.encoding系統屬性讀入的編碼方式來進行編碼,同樣也必須根據系統預設的編碼才能得到正確的結果,這個結果最後要在JAVA中儲存,它還是要從這個encode轉換成Unicode,因為在JAVA中各種字元均是以Unicode的形式來處理的。 l也就是說有bytes-->encode字元-->Unicode字元的轉換;而在String.getBytes([encode])中,系統要做一個Unicode字元-->encode字元-->bytes的轉換。 希望通過本文的介紹能夠使你對字符集編碼的概念以及Java與字符集編碼之間的關係有個清楚的認識,我相信,如果搞清楚了他們之間的關係,那個在Java world鼎鼎有名的亂碼問題將一去不再復返了J
2.字符集 字符集是字元的集合。例如,漢字字元是中國人最先發明的字元,在中文、日文、韓文和越南文的書寫中使用。這也說明了字元和字符集之間的關係,字元組成字符集。
3.編碼字符集 編碼字符集是一個字符集(有時候也被簡稱位字符集),它為每一個字元分配一個唯一數字。最早的編碼是iso8859-1,和ascii編碼相似。但為了方便表示各種各樣的語言,逐漸出現了很多標準編碼。 iso8859-1:
字元編碼方案是從一個或多個編碼字符集到一個或多個固定寬度程式碼單元序列的對映。
最常用的程式碼單元是位元組,所以可以簡單的認為字元編碼方案是為了告訴計算機如何將編碼字符集(如Unicode)對映到計算機可以識別的資料格式中,如位元組。這種編碼方案往往能夠為他所對應的字符集在計算機處理時提供更為優化的空間以及效能上的解決方案。Unicode編碼字符集有三種字元編碼方案,下面將逐一介紹: lUTF-32* 即將每一個Unicode編碼表示為相同值的32位整數。很明顯,它是內部處理最方便的表達方式,但是,如果作為一般字串表達方式,則要消耗更多的記憶體。顯而易見,對於英文字母的表示將需要多個0位元組,僅僅因為我們需要4個位元組32位來表示一個Unicode字元。 lUTF-16 使用一個或兩個未分配的 16位程式碼單元的序列對Unicode編碼進行編碼。值U+0000至U+FFFF 編碼為一個相同值的16位單元。增補字元*編碼為兩個程式碼單元,第一個單元來自於高代理範圍(U+D800 至 U+DBFF),第二個單元來自於低代理範圍(U+DC00 至U+DFFF)。這在概念上可能看起來類似於多位元組編碼,但是其中有一個重要區別:值 U+D800 至 U+DFFF 保留用於 UTF-16;沒有這些值分配字元作為程式碼點。這意味著,對於一個字串中的每個單獨的程式碼單元,軟體可以識別是否該程式碼單元表示某個單單元字元,或者是否該程式碼單元是某個雙單元字元的第一個或第二單元。這相當於某些傳統的多位元組字元編碼來說是一個顯著的改進,在傳統的多位元組字元編碼中,位元組值0x41 既可能表示字母“A”,也可能是一個雙位元組字元的第二個位元組。 lUTF-8 使用一至四個位元組的序列對編碼Unicode進行編碼。U+0000至U+007F使用一個位元組編碼,U+0080至U+07FF 使用兩個位元組,U+0800 至U+FFFF使用三個位元組,而U+10000至U+10FFFF使用四個位元組。UTF-8設計原理為:位元組值0x00至0x7F始終表示程式碼點U+0000至U+007F(Basic Latin 字元子集,它對應 ASCII 字符集)。這些位元組值永遠不會表示其他Unicode編碼字元,這一特性使 UTF-8 可以很方便地在軟體中將特殊的含義賦予某些 ASCII 字元。UTF-8 的格式在編碼英文時,只需要8位,但是中文則是24位,其他更加偏僻的字元才又可能是32位,這也是UTF-8最大的編碼特點,可以最高效率的利用計算機空間,因為在計算機處理的時候大多數情況下還是隻使用英文進行運算和處理,這也是為什麼還需要UTF-8的主要原因,因為畢竟網際網路70%以上的資訊仍然是英文。如果連英文都用2個位元組存取(UCS-2),空間浪費不就太多了? * UTF-32 表示Unicode Transformation Form 32-bit form,UTF-16,UTF-8依此類推。 * Unicode 最初設計是作為一種固定寬度的 16 位字元編碼。在 Java 程式語言中,基本資料型別 char 初衷是通過提供一種簡單的、能夠包含任何字元的資料型別來充分利用這種設計的優點。不過,現在看來,16 位編碼的所有65,536個字元並不能完全表示全世界所有正在使用或曾經使用的字元。於是,Unicode 標準已擴充套件到包含多達 1,112,064個字元。那些超出原來的16位限制的字元被稱作增補字元。
5.Java與編碼字符集 從上面的介紹我們知道了Unicode編碼字符集可以用來表示世界上所有的語言文字。Java內部處理字元使用的字序方式是Unicode,這是一種通行全球的編碼方式,他使Java語言能夠描述世界上所有的文字。在Java程式中對各種字元在記憶體中處理是使用Unicode的UTF-8編碼方式,這也是因為UTF-8的特點所決定的,Class File(也就是bytecode)中有一欄位叫做常數區(Constant Pool),一律使用UTF-8為子元編碼。 這看起來一切正常,Java可以處理世界上所有的字元,一切都是按照秩序在執行,但是,從前面的討論我們知道,世界上並不是僅僅只有Unicode編碼字符集,同時存在的還有iso8859-1、GBK等編碼字符集,就是在Unicode中也同樣存在著UTF-8,UTF-16,UTF-32等多種編碼,如果傳入的位元組編碼採用的是GB18030,而採用的解碼方式為UTF-8那會有什麼後果呢,看看下面的程式碼片段: public static final String TEST_RESOURCE = "你好"; public static void testEncoding() { try { byte[] bytes = TEST_RESOURCE.getBytes("GB18030"); String result = new String(bytes, "UTF-8"); System.out.println("Receive value: [" + result + "]."); } catch (UnsupportedEncodingException e) { // TODO Auto-generated catch block e.printStackTrace(); } } 執行以上的程式碼片段,在我的機器(Win XP中文版)上面得到的結果是: Receive value: [���]. 明白了吧,這就是久負盛名的亂碼問題的根源,目前在市面上存在有多種編碼字符集,以及編碼字符集的編碼方案,所以雖然在Java中內部是以Unicode的UTF-8來處理各種字元的表示以及運算,但是這僅僅是在Java內部而以,如果Java程式需要和外部應用系統進行互動,比如與作業系統,資料庫系統之間的互動,那麼在這些互動過程中如何處理字符集的編碼解碼是解決好Java應用程式亂碼問題的根源。 如果將上面的程式碼塊修改成如下的程式碼塊: public static void testEncoding() { try { byte[] bytes = TEST_RESOURCE.getBytes("GB18030"); String result = new String(bytes, "GB18030"); System.out.println("Receive value: [" + result + "]."); } catch (UnsupportedEncodingException e) { // TODO Auto-generated catch block e.printStackTrace(); } } 注意紅色標註的地方,執行以上的程式碼塊將會受到預期的結果: Receive value: [你好]. 統一字元的編碼型別和解碼型別,如此一來任何亂碼問題都不會再是問題了。在網上可以搜尋到N多的關於如何解決J2EE亂碼問題的文章,我在這裡也就不廢話了,我只是想說說Java亂碼問題的根源之所在。 如果你仔細想想Java的開發過程,原檔案編寫、javac編譯、java執行,這每一步驟都會涉及到編碼的轉換過程,這個過程總是存在的,只是有的時候用預設的引數進行。 我們從javac這個命令來開始我們的分析,編譯的時候,如果你不說明原始檔編碼方式的話,javac 編譯器在讀進此原始程式檔案開始編譯之前,會先去詢問作業系統檔案預設的編碼方式為何。以我的作業系統WIN XP 中文版來說,javac 會先詢問WIN XP,得知當前的編碼是用GB18030的方式編碼。然後就可以將原始檔由GB18030轉成Unicode編碼方式,開始進行編譯。在這裡就會發生一下一些編碼問題: l如果作業系統的國籍資料設定錯誤,會造成javac編譯器取得的編碼資訊是錯誤的,這裡也有可能由於系統屬性file.encoding設定錯誤,在我的系統中該屬性為GB18030,可以通過程式碼System.out.println(System.getProperties());輸出可能的系統屬性。 l較差勁的編譯器可能沒有主動詢問作業系統的編碼方式,而是採用編譯器預設的編碼方式,當然這種情況對於目前先進的編譯器來說已經不存在了,但是這確實是一種可能的原因。 l原始碼是在英文作業系統上書寫採用編碼iso8859-1,寫好以後再將原始碼傳遞給中文作業系統進行編譯,這樣由於兩個作業系統的編碼方式不同,也會造成javac執行錯誤。 明白了吧,這些問題在我們日常的程式碼編寫過程中,往往由於預設的屬性都正好能滿足我們的需要,即原始碼的書寫以及編譯都採用作業系統預設的編碼方式,所以可能很多人到目前為止都沒有遇見過諸如此類的問題,但是我們要知道,這些問題確實是存在的。 Java編譯器在執行過程中給我們提供了可選的encoding引數來告訴編譯器該採用何種編碼方式將讀入的原始檔轉換成Unicode編碼方式,然後再進行後續的編譯工作。 javac –encoding GB18030 …. 6.Inside OK,通過前面的介紹希望讀者能夠對Java以及各種字元編碼之間的關係有個簡單的瞭解,下面我在繼續總結一下: lJavac是以系統預設編碼(file.encoding系統屬性)讀入原始檔,然後按Unicode進行編碼的。 l在JAVA執行的時候,JAVA也是採用Unicode編碼的,為了高度利用記憶體空間提高效率對Unicode字元編碼採用了UTF-8的方式編碼,並且預設輸入和輸出的都是作業系統的預設編碼。 l也就是說在new String(bytes,encode)中,系統認為輸入的是編碼為encode的位元組流,換句話說,如果按encode來翻譯bytes才能得到正確的結果;而在new String(bytes)中採用的就是根據file.encoding系統屬性讀入的編碼方式來進行編碼,同樣也必須根據系統預設的編碼才能得到正確的結果,這個結果最後要在JAVA中儲存,它還是要從這個encode轉換成Unicode,因為在JAVA中各種字元均是以Unicode的形式來處理的。 l也就是說有bytes-->encode字元-->Unicode字元的轉換;而在String.getBytes([encode])中,系統要做一個Unicode字元-->encode字元-->bytes的轉換。 希望通過本文的介紹能夠使你對字符集編碼的概念以及Java與字符集編碼之間的關係有個清楚的認識,我相信,如果搞清楚了他們之間的關係,那個在Java world鼎鼎有名的亂碼問題將一去不再復返了J