世界上的各地區都有本地的語言。地區差異直接導致了語言環境的差異。在開發國際化程序的過程中,處理語言問題就顯得很重要了。
這是一個世界各地都存在的問題,所以,Java提供了世界性的解決方法。本文所描述的方法是用來處理中文的,但是,推而廣之,對於處理世界上其它國家和地區的語言同樣適用。
漢字是雙字節的。所謂雙位元組是指一個雙字要佔用兩個BYTE的位置(即16位元),分別稱為高位元和低位元。中國規定的漢字編碼為GB2312,這是強制性的,目前幾乎所有的能處理中文的應用程式都支援GB2312。 GB2312包括了一二級漢字和9區符號,高位從0xa1到0xfe,低位也是從0xa1到0xfe,其中,漢字的編碼範圍為0xb0a1到0xf7fe。
另外有一種編碼,叫做GBK,但這是一份規範,不是強迫的。 GBK提供了20902個漢字,它相容於GB2312,編碼範圍為0x8140到0xfefe。 GBK中的所有字元都可以一一映射到Unicode 2.0。
在不久的將來,中國會頒布另一種標準:GB18030-2000(GBK2K)。它收錄了藏、蒙等少數民族的字型,從根本解決了字位不足的問題。注意:它不再是定長的。其二位元組部份與GBK相容,四位元組部分是擴充的字元、字形。它的首字節和第三位元組從0x81到0xfe,二位元組和第四位元組從0x30到0x39。
本文不打算介紹Unicode,有興趣的可以瀏覽「http://www.unicode.org/」查看更多的資訊。 Unicode有一個功能:它包括了世界上所有的字元字形。所以,各個地區的語言都可以建立與Unicode的映射關係,而Java正是利用了這一點以達到異種語言之間的轉換。
在JDK中,與中文相關的編碼有:
表1 JDK中與中文相關的編碼列表
編碼名稱 | 說明 |
ASCII | 7位,與ascii7相同 |
ISO8859-1 | 8-位,與8859_1,ISO-8859-1,ISO_8859-1,latin1...等相同 |
GB2312-80 | 16位,與gb2312,gb2312-1980,EUC ,euccn,1381,Cp1381, 1383, Cp1383, ISO2022CN,ISO2022CN_GB...等相同 |
GBK | 與MS936相同,注意:區分大小寫 |
UTF8 | 與UTF-8相同 |
GB18030 | 與cp1392、13922F8與UTF-8相同GB18030與cp1392、1392,目前很少 |
支援程式設計時,接觸得比較多的是GB2312(GBK)和ISO8859-1。
為什麼會有「?」號
上文說過,異種語言之間的轉換是透過Unicode來完成的。假設有兩種不同的語言A和B,轉換的步驟為:先把A轉換成Unicode,再把Unicode轉換成B。
舉例說明。有GB2312中有一個漢字“李”,其編碼為“C0EE”,欲轉化為ISO8859-1編碼。步驟為:先把“李”字轉換為Unicode,得到“674E”,再把“674E”轉換為ISO8859-1字元。當然,這個映射不會成功,因為ISO8859-1中根本沒有與「674E」對應的字元。
當映射不成功時,問題就發生了!當從某語言轉換到Unicode時,如果在某語言中沒有該字符,則得到的將是Unicode的代碼「uffffd」(「u」表示是Unicode編碼,)。而從Unicode轉換成某一語言時,如果某語言沒有對應的字符,則得到的是「0x3f」(「?」)。這就是「?」的由來。
例如:把字元流buf =“0x80 0x40 0xb0 0xa1”進行new String(buf, "gb2312")操作,得到的結果是“ufffdu554a”,再println出來,得到的結果將是“?啊”,因為「0x80 0x40」是GBK中的字符,在GB2312中沒有。
再如,把字串String="u00d6u00ecu00e9u0046u00bbu00f9"進行new String (buf.getBytes("GBK"))操作,得到的結果是“3fa8aca8a6463fa8b4”,其中,“u000d ”在“GBK”中沒有對應的字符,得到“3f”,“u00ec”對應著“a8ac”,“u00e9”對應著“a8a6”,“0046”對應著“46”(因為這是ASCII字符),“u00bb”沒找到,得到“3f”,最後,“u00f9”對應著“a8b4”。把這個字串println一下,得到的結果是「?ìéF?ù」。看到沒?這裡不全是問號,因為GBK與Unicode映射的內容中除了漢字外還有字符,本例就是最好的明證。
所以,在漢字轉碼時,如果發生錯亂,得到的不一定都是問號喔!不過,錯了終究是錯了,50步和100步並沒有質的差別。
或會問:如果原始字元集中有,而Unicode沒有,結果會如何?回答是不知道。因為我手邊沒有能做這個測試的來源字元集。但有一點是肯定的,那就是原始字元集不夠規範。在Java中,如果發生這種情況,是會拋出異常的。
什麼是UTF
UTF,是Unicode Text Format的縮寫,意思是Unicode文字格式。對於UTF,是這樣定義的:
(1)如果Unicode的16位元字元的頭9位元是0,則用一個位元組表示,這個位元組的首位是“0”,剩下的7位元與原字元中的後7位元相同,如“u0034”(0000 0000 0011 0100),用“34” (0011 0100)表示;(與來源Unicode字元是相同的);
(2)如果Unicode的16位元字元的頭5位元是0,則用2個位元組表示,首字節是“110”開頭,後面的5位元與原始字元中除去頭5個零後的最高5位元相同;第二個位元組以“10”開頭,後面的6位元與原始字元中的低6位元相同。如「u025d」(0000 0010 0101 1101),轉換後為「c99d」(1100 1001 1001 1101);
(3)如果不符合上述兩個規則,則以三個位元組表示。第一個位元組以「1110」開頭,後四位元為原始字元的高四位元;第二位元組以「10」開頭,後六位元為原始字元中間的六位元;第三位元組以
「10」開頭,後六位
為原始字元的低六位元;如「u9da7」(1001 1101 1010 0111),轉換為「e9b6a7」(1110 1001 1011 0110 1010 0111);
UTF的關係,雖然不絕對:字串在記憶體中運行時,表現為Unicode程式碼,而當要儲存到檔案或其它媒體中去時,用的是UTF。這個轉換過程是由writeUTF和readUTF來完成的。
好了,基礎性的論述差不多了,下面進入正題。
先把這個問題想成是一個黑盒子。先看黑盒子的一級表示:
input(charsetA)->process(Unicode)->output(charsetB)
簡單,這就是一個IPO模型,也就是輸入、處理和輸出。同樣的內容要經過「從charsetA到unicode再到charsetB」的轉換。
再看二級表示:
SourceFile(jsp,java)->class->output
在這個圖中,可以看出,輸入的是jsp和java來源文件,在處理過程中,以Class文件為載體,然後輸出。再細化到三級表示:
jsp->temp file->class->browser,os console,db
app,servlet->class->browser,os console,db
這張圖就更懂了。 Jsp文件先生成中間的Java文件,再產生Class。而Servlet和普通App則直接編譯產生Class。然後,從Class再輸出到瀏覽器、控制台或資料庫等。
JSP:從原始檔到Class的過程
Jsp的原始檔是以「.jsp」結尾的文字檔。在本節中,將闡述JSP文件的解釋和編譯過程,並追蹤其中的中文變化。
1.JSP/Servlet引擎提供的JSP轉換工具(jspc)搜尋JSP檔案中以<%@ page contentType ="text/html; charset=<Jsp-charset>"%>中指定的charset。如果在JSP檔案中未指定<Jsp-charset>,則取JVM中的預設設定file.encoding,一般情況下,這個值是ISO8859-1;
2、jspc用相當於「javac –encoding <Jsp-charset> 」的命令解釋JSP文件中出現的所有字符,包括中文字符和ASCII字符,然後把這些字符轉換成Unicode字符,再轉化成UTF格式,存為JAVA文件。 ASCII碼字元轉換為Unicode字元時只是簡單地在前面加上“00”,如“A”,轉換為“u0041”(不需要理由,Unicode的碼表就是這麼編的)。然後,經過UTF的轉換,又變回「41」了!這也就是可以用普通文字編輯器查看由JSP產生的JAVA檔的原因;
3.引擎用相當於「javac –encoding UNICODE」的指令,把JAVA檔編譯成CLASS檔;
先看這些過程中中文字符的轉換情況。有以下原始碼:
<%@ page contentType="text/html; charset=gb2312"%>
<html><body>
<%
String a="中文";
out.println(a);
%>
</body></html>
這段程式碼是在UltraEdit for Windows上寫的。儲存後,「中文」兩個字的16進位編碼為「D6 D0 CE C4」(GB2312編碼)。經查表,「中文」二字的Unicode編碼為“u4E2Du6587”,用UTF表示就是“E4 B8 AD E6 96 87”。開啟引擎產生的由JSP文件轉變而成的JAVA文件,發現其中的「中文」兩個字確實被「E4 B8 AD E6 96 87」取代了,再查看由JAVA文件編譯產生的CLASS文件,發現結果與JAVA檔案中的完全一樣。
再來看JSP中指定的CharSet為ISO-8859-1的情況。
<%@ page contentType="text/html; charset=ISO-8859-1"%>
<html><body>
<%
String a="中文";
out.println(a);
%>
</body></html>
同樣,該檔案是用UltraEdit編寫的,「中文」這兩個字也是存為GB2312編碼「D6 D0 CE C4」。先模擬產生的JAVA檔案和CLASS檔案的流程:jspc用ISO-8859-1來解釋“中文”,並把它對應到Unicode。由於ISO-8859-1是8位的,且是拉丁語系,其映射規則就是在每個位元組前加“00”,所以,映射後的Unicode編碼應為“u00D6u00D0u00CEu00C4” ,轉換成UTF後應該是「C3 96 C3 90 C3 8E C3 84」。好,打開文件看一下,JAVA檔案和CLASS檔案中,「中文」果然都表示為「C3 96 C3 90 C3 8E C3 84」。
如果上述程式碼中不指定<Jsp-charset>,也就是把第一行寫成“<%@ page contentType="text/html" %>”,JSPC會使用file.encoding的設定來解釋JSP檔。在RedHat 6.2上,其處理結果與指定為ISO-8859-1是完全相同的。
到目前為止,已經解釋了從JSP檔案到CLASS檔案的轉變過程中中文字元的對應過程。一句話:從「JspCharSet到Unicode再到UTF」。下表總結了這個過程:
表2 「中文」從JSP到CLASS的轉換過程
Jsp-CharSet | JSP檔案中 | JAVA檔案中 | CLASS檔案中 | |
GB2312 | D6 D0 CE C4(GB2312) | 從u4E2Du6587(Unicode)到E4 B8 AD E6 96 87 (UTF) | E4 B8 AD E6 96 87 (UTF) ISO-8859) | |
ISO-8859 | ||||
-1 | D6 D0 CE C4 (GB2312) | 從u00D6u00D0u00CEu00C4 (Unicode)到C3 96 C3 90 C3 8E C3 84 (UTF) | C3 96 C3 90 C3 8E C3 84 (UTF) | |
無(預設) | 同同 | file.-1 | 同ISO-8859-1 | 同ISO-8859-1 |
Servlet:從原始檔到Class的程序
Servlet原始檔是以「.java」結尾的文字檔。本節將討論Servlet的編譯過程並追蹤其中的中文變化。
用“javac”編譯Servlet原始檔。 javac可以帶「-encoding <Compile-charset>」參數,意思是「用< Compile-charset >中指定的編碼來解釋Serlvet原始檔」。
原始檔在編譯時,用<Compile-charset>來解釋所有字符,包括中文字符和ASCII字符。然後把字符常數轉換成Unicode字符,最後,把Unicode變成UTF。
在Servlet中,還有一個地方設定輸出流的CharSet。通常在輸出結果前,呼叫HttpServletResponse的setContentType方法來達到與在JSP中設定<Jsp-charset>相同的效果,稱為<Servlet-charset>。
請注意,文中一共提到了三個變數:<Jsp-charset>、<Compile-charset>和<Servlet-charset>。其中,JSP檔案只與<Jsp-charset>有關,而<Compile-charset>和<Servlet-charset>只與Servlet有關。
看下範例:
import javax.servlet.*;
import javax.servlet.http.*;
class testServlet extends HttpServlet
{
public void doGet(HttpServletRequest req,HttpServletResponse resp)
throws ServletException,java.io.IOException
{
resp.setContentType("text/html; charset=GB2312");
java.io.PrintWriter out=resp.getWriter();
out.println("<html>");
out.println("#中文#");
out.println("</html>");
}
}
該檔案也是用UltraEdit for Windows編寫的,其中的「中文」兩個字保存為「D6 D0 CE C4」(GB2312編碼)。
開始編譯。下表是<Compile-charset>不同時,CLASS檔案中「中文」兩字的十六進位碼。在編譯過程中,<Servlet-charset>不起任何作用。 <Servlet-charset>只對CLASS檔案的輸出產生影響,實際上是<Servlet-charset>和<Compile-charset>一起,達到與JSP檔案中的<Jsp-charset>相同的效果,因為<Jsp-charset >對編譯和CLASS檔案的輸出都會產生影響。
表3 「中文」從Servlet原始檔到Class的轉變過程
Compile-charset | Servlet來源檔案中 | Class檔案中 | 等效的Unicode碼 | |
GB2312 | D6 D0 CE C4 (GB2312) | E4 B8 AD E6 96 87 (UTF) | u4E2Du6587 (在Unicode=「中文」) | |
ISO-8859-1 | D6 D0 CE C4 (GB2312) | C3 96 C3 90 C3 8E C3 84 (UTF) | u00D6 u00D0 u00CE u00C4 (在D6 D0 CE C4前面各加了一個00) | |
無(預設) | D6 D0 CE C4 (GBISO | -859 | )-1 | 同ISO-8859-1 |
序號 | 步驟說明 | 結果 |
1 | 編寫JSP來源文件,且存為GB2312格式 | D6 D0 CE C4 (D6D0=中CEC4=文) |
2 | jspc把JSP原始檔轉化為臨時JAVA文件,並把字串依照GB2312映射到Unicode,並用UTF格式寫入JAVA文件中 | E4 B8 AD E6 96 87 |
3 | 把臨時JAVA文件編譯成CLASS檔案 | E4 B8 AD E6 96 87 |
4 | 運行時,先從CLASS檔案中用readUTF讀出字串,在記憶體中的是Unicode編碼 | 4E 2D 65 87(在Unicode中4E2D=中6587=文) |
5 | 根據Jsp -charset=GB2312把Unicode轉換為位元組流 | D6 D0 CE C4 |
6 | 把位元組流輸出到IE中,並設定IE的編碼為GB2312(作者按:這個資訊隱藏在HTTP頭中) | D6 D0 CE C4 |
7 | IE用“簡體中文”查看結果 | “中文”(正確顯示) |
序號 | 步驟說明 | 結果 |
1 | 編寫JSP來源文件,且存為GB2312格式 | D6 D0 CE C4 (D6D0=中CEC4=文) |
2 | jspc把JSP原始檔轉換成臨時JAVA文件,並把字串依照ISO8859-1對應到Unicode,並用UTF格式寫入JAVA文件中 | C3 96 C3 90 C3 8E C3 84 |
3 | 把臨時JAVA檔案編譯成CLASS檔案 | C3 96 C3 90 C3 8E C3 84 |
4 | 執行時,先從CLASS檔案中用readUTF讀出字串,在記憶體中的是Unicode編碼 | 00 D6 00 D0 00 CE 00 C4 (啥都不是!!!) |
5 | 根據Jsp-charset=ISO8859-1把Unicode轉換為位元組流 | D6 D0 CE C4 |
6 | 把位元組流輸出到IE中,並設定IE的編碼為ISO8859-1(作者按:這個訊息隱藏在HTTP頭中) | D6 D0 CE C4 |
7 | IE用「西歐字符」查看結果 | 亂碼,其實是四個ASCII字符,但由於大於128,所以顯示出來的怪模怪樣 |
8 | 改變IE的頁面編碼為「簡體中文” | “中文”(正確顯示) |
序號 | 步驟說明 | 結果 |
1 | 編寫JSP來源文件,且存為GB2312格式 | D6 D0 CE C4 (D6D0=中CEC4=文) |
2 | jspc把JSP原始檔轉換成臨時JAVA文件,並把字串依照ISO8859-1對應到Unicode,並用UTF格式寫入JAVA文件中 | C3 96 C3 90 C3 8E C3 84 |
3 | 把臨時JAVA文件編譯成CLASS文件 | C3 96 C3 90 C3 8E C3 84 |
4 | 運行時,先從CLASS文件中用readUTF讀出字符串,在內存中的是Unicode編碼 | 00 D6 00 D0 00 CE 00 C4 |
5 | 根據Jsp- charset=ISO8859-1把Unicode轉換為位元組流 | D6 D0 CE C4 |
6 | 把位元組流輸出到IE中 | D6 D0 CE C4 |
7 | IE用發出請求時的頁面的編碼查看結果 | 視情況而定。如果是簡體中文,則能正確顯示,否則,需執行表5中的第8步 |
序號 | 步驟說明 | 結果 |
1 | 編寫Servlet來源文件,且存為GB2312格式 | D6 D0 CE C4 (D6D0=中CEC4=文) |
2 | 用javac –encoding GB2312把JAVA原始檔編譯成CLASS檔 | E4 B8 AD E6 96 87 (UTF) |
3 | 執行時,先從CLASS檔中用readUTF讀出字串,在記憶體中的是Unicode編碼 | 4E 2D 65 87 (Unicode) |
4 | 根據Servlet-charset=GB2312把Unicode轉換為位元組流 | D6 D0 CE C4 (GB2312) |
5 | 把位元組流輸出到IE中並設定IE的編碼屬性為Servlet- charset=GB2312 | D6 D0 CE C4 (GB2312) |
6 | IE用「簡體中文」檢視結果 | 「中文」(正確顯示) |
序號 | 步驟說明 | 結果 |
1 | 編寫Servlet來源文件,且存為GB2312格式 | D6 D0 CE C4 (D6D0=中CEC4=文) |
2 | 用javac –encoding ISO8859-1把JAVA原始檔編譯成CLASS檔 | C3 96 C3 90 C3 8E C3 84 (UTF) |
3 | 執行時,先從CLASS檔中用readUTF讀出字串,在記憶體中的是Unicode編碼 | 00 D6 00 D0 00 CE 00 C4 |
4 | 根據Servlet-charset=ISO8859-1把Unicode轉換為位元組流 | D6 D0 CE C4 |
5 | 把位元組流輸出到IE中並設定IE的編碼屬性為Servlet-charset=ISO8859-1 | D6 D0 CE C4 (GB2312) |
6 | IE用「西歐字元」查看結果 | 亂碼(原因同表5) |
7 | 改變IE的頁面編碼為「簡體中文」 | 「中文」(正確顯示) |
序號 | 步驟說明 | 結果 | 域 |
1 | 在IE中輸入「中文」 | D6 D0 CE C4 | IE |
2 | IE把字串轉變成UTF,並送入傳輸流中 | E4 B8 AD E6 96 87 | |
3 | Servlet接收到輸入流,用readUTF讀取 | 4E 2D 65 87(unicode) | Servlet |
4 | 程式設計者在Servlet中必須把字串根據GB2312還原為位元組流 | D6 D0 CE C4 | |
5 | 程式設計者根據資料庫內碼ISO8859-1產生新的字串 | 00 D6 00 D0 00 CE 00 C4 | |
6 | 把新產生的字串提交給JDBC | 00 D6 00 | |
7 | JDBC偵測到資料庫內碼為ISO8859-1 | 00 D6 00 D0 00 CE 00 C4 | JDBC |
8 | JDBC把接收到的字串依照ISO8859 -1產生位元組流 | D6 D0 CE C4 | |
9 | JDBC把位元組流寫入資料庫中 | D6 D0 CE C4 | |
10 | 完成資料儲存工作 | D6 D0 CE C4 資料庫 | |
以下是從資料庫中取出數的過程 | |||
11 | JDBC從資料庫中取出字節流 | D6 D0 CE C4 | JDBC |
12 | JDBC依照資料庫的字元集ISO8859-1產生字串,並提交給Servlet | 00 D6 00 D0 00 CE 00 C4 (Unicode) | |
13 | Servlet取得字串 | 00 D6 00 D0 00 CE 00 C4 (Unicode) | Servlet |
14 | 程式設計者必須依照資料庫的內碼ISO8859-1還原成原始位元組流 | D6 D0 CE C4 | |
15 | 編程者必須根據客戶端字元集GB2312產生新的字串 | 4E 2D 65 87 (Unicode) | |
Servlet準備把字串輸出到客戶端 | |||
16 | Servlet根據<Servlet-charset>產生位元組流 | D6D0 CE C4 | Servlet |
17 | Servlet把位元組流輸出到IE中,如果已指定<Servlet-charset>,也會設定IE的編碼為<Servlet-charset> | D6 D0 CE C4 | |
18 | IE根據指定的編碼或預設編碼查看結果 | 「中文」(正確顯示) | IE |