Java IO4:字元編碼

五月的倉頡發表於2015-10-18

前言

字元編碼,這本不屬於IO的內容,但位元組流之後寫的應該是字元流,既然是字元流,那就涉及一個"字元編碼的"問題,考慮到字元編碼不僅僅是在IO這塊,Java中很多場景都涉及到這個概念,因此這邊文章就專門詳細寫一下字元編碼,具體的網上有很多,但本文目的是儘量講清楚各種編碼方式的作用,個人認為,不求、也沒有必要對字元編碼理解地多麼深入。

 

字符集和字元編碼

第一個概念就是字符集和字元編碼之間的區別:

1、字符集(charset)

字符集指的是一個系統支援的所有抽象字元的集合。字元是各種文字和符號的總稱,包括各國家文字、標點符號、圖形符號、數字等,常見的字符集有ASCII字符集、GB2312字符集、BIG5字符集、GB18030字符集、Unicode字符集等。

2、字元編碼(encoding)

計算機要準確處理各種字符集文字,就要進行字元編碼,以便計算機能夠識別和儲存各種文字。因此字元編碼就是講符號轉換為計算機可以接受的數字系統的數,稱為數字程式碼。

 

ASCII碼

計算機裡面只有數字0和1(嚴格說連0和1都沒有,只有開和關,無非是用0和1表示開關的狀態罷了),在計算機軟體裡的一切都是用數字標識的額,螢幕上顯示的一個一個字元也是數字。最初使用的計算機在美國,用到的字元很少,因此每一個字元都用一個數字表示,一個位元組所能表示的數字反內衛足以容納所有這些字元。實際上表示這些字元的數字的位元組最高位都是0,也就是說這些數字都在0~127之間,如字元a對應97,字元b對應數字98,這種字元與數字的對應編碼固定下來之後,這套編碼規則被稱為ASCII碼(美國標準資訊交換碼)。一張簡單的ASCII碼錶如圖:

從表中可以看出ASCII碼分為兩部分:

1、0~31是控制字元,如換行、回車、刪除等

2、32~126是列印字元,可以通過鍵盤輸入並且能夠顯示出來

 

GB2312和GBK

隨著計算機在其它國家的普及,許多國家把本地字符集引入了計算機,大大擴充套件了計算機中字元的範圍。一個位元組所能表示的範圍不足以容納中文字元(看看上面的ASCII碼錶就知道了),中國大陸將每一箇中文字元都用兩個位元組表示,原有的ASCII碼字元的編碼保持不變。

為了將一箇中文字元與兩個ASCII碼字元相區別,中文字元的每個位元組最高位為1,中國大陸為每一箇中文字元都指定了一個對應的數字,並於1980年制定了一套《資訊科技 中文編碼字符集》,這套規範就是GB2312。GB2312是雙位元組編碼,總的編碼範圍是A1~F7,其實A1~A9是富豪區,總共包含682個符號;B0~F7是漢字區,總共包含6763個漢字。

GBK是在1995年制定的後續標準,全稱為《漢字內碼擴充套件規範》,是國家技術監督局為Windows 95所制定的新的漢字內碼規範。GBK的出現是為了擴充套件GBK2312,並加入更多的漢字。GBK的編碼範圍是8140~FEFE(去掉XX7F),總共有23940個碼位,能表示21003個漢字,它的編碼是和GB2312相容的,也就是說用GB2312編碼的漢字可以用GBK來解碼,並且不會有亂碼問題。GBK還是現如今中文Windows作業系統的系統預設編碼。

 

Unicode

在一個國家的本地化系統中出現的一個字元,通過電子郵件傳送到另外一個國家的本地化系統中,看到的就不是那個原始字元了,而是另外那個國家的一個字元或亂碼,因為計算機裡面並沒有真正的字元,字元都是以數字的形式存在的,通過郵件傳送一個字元,實際上傳送的是這個字元對應的字元編碼,同一個數字在不同的國家和地區代表的很可能是不同的符號。

為了解決各個國家和地區之間各自使用不同的本地化字元編碼帶來的不便,人們將全世界所有的符號進行了統一編碼,稱之為Unicode(統一碼、萬國碼)。所有字元不再區分國家和地區,都是人類共有的符號,如"中"字在Unicode中不再是GBK中的D6D0,而是在任何地方都是4e2d,如果所有的計算機系統都使用這種編碼方式,那麼4e2d這個字在任何地方都代表漢字中的"中"。Unicode編碼的字元都佔用兩個位元組的大小,也就是說全世界所有字元個數不會超過65536個。

當然Unicode只包含65536個字元就想包含全世界所有的字元是遠遠不夠的,所以Unicode提供了字元平面對映,連結地址上就是Wiki百科對於字元平面對映的解讀。另外要提一點的是,Unicode是Java和XML的基礎。

 

UTF-8和UTF-16

Unicode是一種字符集標準,而具體該標準應該如何應用到計算機中,則是另一個話題了,常用的Unicode編碼方式有兩種:

1、UTF-16。兩個位元組表示Unicode轉換格式,這是定長的表示方法。也就是說不管什麼字元都可以使用兩個位元組表示,兩個位元組是16Bit,所以叫做UTF-16。UTF-16編碼非常方便,每兩個位元組表示一個字元,這個在字串操作時大大簡化了操作。

2、UTF-8。UTF-16統一採用了兩個位元組表示一個字元,雖然在表示上非常簡單,但是很大一部分字元用一個位元組表示就夠了,現在需要兩個位元組,儲存空間放大了一倍。UTF-8就採取了一種變長技術,每個編碼區域有不同的字碼長度,不同型別的字元可以是由1~6個位元組組成。

兩種編碼方式比較,相對來說,UTF-16的編碼效率較高,從字元到位元組的相互轉換可以更簡單,進行字串操作也更好,它更適合在本地磁碟和記憶體之間使用,可以進行字元和位元組之間的快速切換。但是UTF-16並不適合在網路之間傳輸,因為網路傳輸易損壞位元組流,一旦位元組流損壞將很難恢復,所以相比較而言UTF-8更適合網路傳輸。另外UTF-8對ASCII字元采用單位元組儲存,單個字元損壞也不會影響後面的其他字元,在編碼效率上介於GBK和UTF-16之間,所以,UTF-8在編碼效率和編碼安全性上做了平衡,是理想的中文編碼方式。

 

Java與字元編碼

Java中的字元使用的都是Unicode字符集,編碼方式為UTF-16,Java技術在通過Unicode保證跨平臺特性的前提下也支援了全擴充套件的本地平臺字符集,而顯示輸出和鍵盤輸入都是採用的本地編碼。因此,免不了二者的轉化問題。

看一個很簡單的例子:

public static void main(String[] args) throws Exception
{
    // 這裡將字串通過getBytes()方法,編碼成GB2312
    byte b[] = "大家一起來學習Java語言".getBytes("GB2312");
    File file = new File("D:/Files/encoding.txt");
    OutputStream out = new FileOutputStream(file);
    out.write(b);
    out.close();
}

看一下檔案中是什麼:

正常輸出,無編碼問題,但是如果這樣:

public static void main(String[] args) throws Exception
{
    // 這裡將字串通過getBytes()方法,編碼成GB2312
    byte b[] = "大家一起來學習Java語言".getBytes("ISO8859-1");
    File file = new File("D:/Files/encoding.txt");
    OutputStream out = new FileOutputStream(file);
    out.write(b);
    out.close();
}

再看一下檔案中是什麼:

亂碼問題就出現了,通過上述操作的完整過程分析一下原因。

要再次說明的是,Java中的String都是Unicode字符集的。Java中的各個類,對於英文字元的支援都非常好,可以正常地寫入檔案中,但對於中文字元就未必了。從Java原始碼到輸入檔案正確的內容,要經過"Java原始碼->Java位元組碼->虛擬機器->檔案"幾個步驟,在上述過程中的每一步都必須正確地處理漢字的編碼,才能夠使最終有我們期望的結果。

"Java原始碼->Java位元組碼",標準的Java編譯器Javac使用的字符集是系統預設的字符集,比如在中文Windows作業系統上就是GBK(上面GBK的部分已經說明過了),而在Linux作業系統上就是ISO8859-1,所以大家會發現Linux作業系統上編譯的類中原始檔中的中文字元都出現了問題,解決辦法就是在編譯的時候新增encoding引數,這樣才能夠與平臺無關,用法是:javac -encoding GBK。

"Java位元組碼->虛擬機器->檔案",Java執行環境(JRE)分英文版和國際版,但只有國際版才支援非英文字元。Java開發工具包(JDK)肯定支援多國字元,但並非所有的計算機使用者都安裝了JDK。很多作業系統應用軟體為了能夠更好地支援Java,都內嵌了JRE的國際版本,為支援自己多國字元提供了方便。

問題就出"Java原始碼->Java位元組碼上",這是由於JDK設定環境變數引起的。用程式看一下JDK環境變數:

public static void main(String[] args)
{
    System.getProperties().list(System.out);
}

看一下輸出的全部資訊,有點長:

 1 -- listing properties --
 2 java.runtime.name=Java(TM) SE Runtime Environment
 3 sun.boot.library.path=E:\MyEclipse10\Common\binary\com.sun....
 4 java.vm.version=11.3-b02
 5 java.vm.vendor=Sun Microsystems Inc.
 6 java.vendor.url=http://java.sun.com/
 7 path.separator=;
 8 java.vm.name=Java HotSpot(TM) 64-Bit Server VM
 9 file.encoding.pkg=sun.io
10 user.country=CN
11 sun.java.launcher=SUN_STANDARD
12 sun.os.patch.level=
13 java.vm.specification.name=Java Virtual Machine Specification
14 user.dir=F:\程式碼\MyEclipse\TestIO
15 java.runtime.version=1.6.0_13-b03
16 java.awt.graphicsenv=sun.awt.Win32GraphicsEnvironment
17 java.endorsed.dirs=E:\MyEclipse10\Common\binary\com.sun....
18 os.arch=amd64
19 java.io.tmpdir=C:\Users\dell1\AppData\Local\Temp\
20 line.separator=
21 
22 java.vm.specification.vendor=Sun Microsystems Inc.
23 user.variant=
24 os.name=Windows Vista
25 sun.jnu.encoding=GBK
26 java.library.path=E:\MyEclipse10\Common\binary\com.sun....
27 java.specification.name=Java Platform API Specification
28 java.class.version=50.0
29 sun.management.compiler=HotSpot 64-Bit Server Compiler
30 os.version=6.2
31 user.home=C:\Users\dell1
32 user.timezone=
33 java.awt.printerjob=sun.awt.windows.WPrinterJob
34 file.encoding=GBK
35 java.specification.version=1.6
36 user.name=dell1
37 java.class.path=F:\程式碼\MyEclipse\TestIO\bin
38 java.vm.specification.version=1.0
39 sun.arch.data.model=64
40 java.home=E:\MyEclipse10\Common\binary\com.sun....
41 java.specification.vendor=Sun Microsystems Inc.
42 user.language=zh
43 awt.toolkit=sun.awt.windows.WToolkit
44 java.vm.info=mixed mode
45 java.version=1.6.0_13
46 java.ext.dirs=E:\MyEclipse10\Common\binary\com.sun....
47 sun.boot.class.path=E:\MyEclipse10\Common\binary\com.sun....
48 java.vendor=Sun Microsystems Inc.
49 file.separator=\
50 java.vendor.url.bug=http://java.sun.com/cgi-bin/bugreport...
51 sun.cpu.endian=little
52 sun.io.unicode.encoding=UnicodeLittle
53 sun.desktop=windows
54 sun.cpu.isalist=amd64

注意一下34行,表明了JDK使用的是GBK字符集(GBK是GB2312上的擴充套件,所以用GB2312字符集當然是沒有問題的),這意味著Java對String的操作,都做了Unicode到GBK的轉換。既然JDK用的GBK編碼,那麼用ISO8859-1字符集顯示GBK編碼出來的中文當然是有問題的。

這只是一個例子,在我們的應用程式中涉及I/O操作時,一般只要注意指定統一的編解碼Charset集,就不會出現亂碼問題。對有些應用程式如果不注意指定字元編碼,則在中文環境中會使用作業系統的預設編碼。如果編解碼都在中文環境中,通常也沒有問題,但還是強烈建議不要使用作業系統的預設編碼,因為這樣會使你的應用程式的編碼格式和執行時環境繫結起來,這樣在跨環境時很可能出現亂碼問題。

相關文章