如果你是JVM的設計者,讓你來決定JVM中所有字符的表示形式,你會不會允許使用各種編碼方式的字符並存?
我想你的答案是不會,如果在內存中的Java字符可以以GB2312,UTF-16,BIG5等各種編碼形式存在,那麼對開發者來說,連進行最基本的字符串打印、連接等操作都會寸步難行。例如一個GB2312的字符串後面連接一個UTF-8的字符串,那麼連接後的最終結果應該是什麼編碼的呢?你選哪一個都沒有道理。
因此牢記下面這句話,這也是Java開發者的共同意志:在Java中,字符只以一種編碼形式存在,那就是UTF-16。
但“在Java中”到底是指在哪裡呢?就是指在JVM中,在內存中,在你的代碼裡聲明的每一個char,String類型的變量中。例如你在程序中這樣寫
char han='漢';
在內存的相應區域,這個字符就表示為0x6C49。可以用下面的代碼證明一下:
char han='漢';
System.out.format("%x",(short)han);
輸出是:
6c49
反過來用UTF-16編碼來指定一個字符也可以,像這樣:
char han=0x6c49;
System.out.println(han);
輸出是:
漢
這其實也是說,只要你正確的讀入了“漢”這個字,那麼它在內存中的表示形式一定是0x6C49,沒有任何其他的值能代表這個字(當然,如果你讀錯了,那結果是什麼就不知道了,范偉說:讀,讀錯了呀,那還等於好幾億呢;本山大哥說:好幾億你也沒答上,請聽下一題)。
JVM的這種約定使得一個字符存在的世界分為了兩部分:JVM內部和OS的文件系統。在JVM內部,統一使用UTF-16表示,當這個字符被從JVM內部移到外部(即保存為文件系統中的一個文件的內容時),就進行了編碼轉換,使用了具體的編碼方案(也有一種很特殊的情況,使得在JVM內部也需要轉換,不過這個是後話)。
因此可以說,所有的編碼轉換就只發生在邊界的地方,JVM和OS的交界處,也就是你的各種輸入輸出流(或者Reader,Writer類)起作用的地方。
話頭扯到這裡就必須接著說Java的IO系統。
盡管看上去混亂繁雜,但是所有的IO基本上可以分為兩大陣營:面向字符的Reader啊Wrtier啊,以及面向字節的輸入輸出流。
下面我來逐一分解,其實一點也不難。
面向字符和面向字節中的所謂“面向”什麼,是指這些類在處理輸入輸入的時候,在哪個意義上保持一致。如果面向字節,那麼這類工作要保證系統中的文件二進制內容和讀入JVM內部的二進制內容要一致。不能變換任何0和1的順序。因此這是一種非常“忠實於原著”的做法(偶然間讓我想起郭敬明抄襲莊羽的文章,那家伙,太忠實於原著了,笑)。
這種輸入輸出方式很適合讀入視頻文件或者音頻文件,或者任何不需要做變換的文件內容。
而面向字符的IO是指希望系統中的文件的字符和讀入內存的“字符”(注意和字節的區別)要一致。例如我們的中文版WindowsXP系統上有一個GBK的文本文件,其中有一個“漢”字,這個字的GBK編碼是0xBABA(而UTF-16編碼是0x6C49),當我們使用面向字符的IO把它讀入內存並保存在一個char型變量中時,我希望IO系統不要傻傻的直接把0xBABA放到這個char型變量中,我甚至都不關心這個char型變量具體的二進制內容到底是多少,我只希望這個字符讀進來之後仍然是“漢”這個字。
從這個意義上也可以看出,面向字符的IO類,也就是Reader和Writer類,實際上隱式的為我們做了編碼轉換,在輸出時,將內存中的UTF-16編碼字符使用系統默認的編碼方式進行了編碼,而在輸入時,將文件系統中已經編碼過的字符使用默認編碼方案進行了還原。我兩次提到“默認”,是說Reader和Writer的聰明也僅此而已了,它們只會使用這個默認的編碼來做轉換,你不能為一個Reader或者Writer指定轉換時使用的編碼。這也意味著,如果你使用中文版WindowsXP系統,而上面存放了一個UTF-8編碼的文件,當你使用Reader類來讀入的時候,它會傻傻的使用GBK來做轉換,轉換後的內容當然驢唇不對馬嘴!
這種笨,有時候其實是一種傻瓜式的功能提供方式,對大多數初級用戶(以及不需要跨平台的高級用戶)來說反而是件好事。
但我們不一樣啦,我們都是國家棟梁,肩負著趕英超美的責任,必須師夷長技以治夷,所以我們總還要和GBK編碼以外的文件打交道。
說了上面這些內容,想必聰明的讀者已經看出來,所謂編碼轉換就是一個字符與字節之間的轉換,因此Java的IO系統中能夠指定轉換編碼的地方,也就在字符與字節轉換的地方,那就是(讀者:InputSteamReader和OutputStreamWriter!作者:太強了,都會搶答了!)
這兩個類是字節流和字符流之間的適配器類,因此他們肩負著編碼轉換的任務簡直太自然啦!要注意,實際上也只能在這兩類實例化的時候指定編碼,是不是很好記呢?
下面來寫一段小程序,來把“漢”字用我們非常崇拜的UTF-8編碼寫到文件中!
try{ PrintWriter out=new PrintWriter(new OutputStreamWriter(new FileOutputStream("c:/utf-8.txt"),"UTF-8")); try{ out.write("漢"); }finally{ out.close(); } }catch(IOException e){ throw new RuntimeException(e); }
運行之後到c盤下去找utf-8.txt這個文件,用UltraEdit打開,使用16進制查看,看到了什麼?它的值是0xE6B189!(這正是“漢”這個字的UTF-8編碼)噢耶!(讀者:這,這有什麼好高興的……)
下一節我們來看看實現這種操作的其他方式,讀到這裡,你已經基本上是字符編碼的高手了哦。