如果你是JVM的設(shè)計(jì)者,讓你來(lái)決定JVM中所有字符的表示形式,你會(huì)不會(huì)允許使用各種編碼方式的字符并存?
我想你的答案是不會(huì),如果在內(nèi)存中的Java字符可以以GB2312,UTF-16,BIG5等各種編碼形式存在,那么對(duì)開(kāi)發(fā)者來(lái)說(shuō),連進(jìn)行最基本的字符串打印、連接等操作都會(huì)寸步難行。例如一個(gè)GB2312的字符串后面連接一個(gè)UTF-8的字符串,那么連接后的最終結(jié)果應(yīng)該是什么編碼的呢?你選哪一個(gè)都沒(méi)有道理。
因此牢記下面這句話,這也是Java開(kāi)發(fā)者的共同意志:在Java中,字符只以一種編碼形式存在,那就是UTF-16。
但“在Java中”到底是指在哪里呢?就是指在JVM中,在內(nèi)存中,在你的代碼里聲明的每一個(gè)char,String類型的變量中。例如你在程序中這樣寫(xiě)
char han='';

在內(nèi)存的相應(yīng)區(qū)域,這個(gè)字符就表示為0x6C49。可以用下面的代碼證明一下:
char han='';
System.out.format(
"%x",(short)han);

輸出是:
6c49
反過(guò)來(lái)用UTF-16編碼來(lái)指定一個(gè)字符也可以,像這樣:
char han=0x6c49;
System.out.println(han);

輸出是:

這其實(shí)也是說(shuō),只要你正確的讀入了“漢”這個(gè)字,那么它在內(nèi)存中的表示形式一定是0x6C49,沒(méi)有任何其他的值能代表這個(gè)字(當(dāng)然,如果你讀錯(cuò)了,那結(jié)果是什么就不知道了,范偉說(shuō):讀,讀錯(cuò)了呀,那還等于好幾億呢;本山大哥說(shuō):好幾億你也沒(méi)答上,請(qǐng)聽(tīng)下一題)。

JVM的這種約定使得一個(gè)字符存在的世界分為了兩部分:JVM內(nèi)部和OS的文件系統(tǒng)。在JVM內(nèi)部,統(tǒng)一使用UTF-16表示,當(dāng)這個(gè)字符被從JVM內(nèi)部移到外部(即保存為文件系統(tǒng)中的一個(gè)文件的內(nèi)容時(shí)),就進(jìn)行了編碼轉(zhuǎn)換,使用了具體的編碼方案(也有一種很特殊的情況,使得在JVM內(nèi)部也需要轉(zhuǎn)換,不過(guò)這個(gè)是后話)。
因此可以說(shuō),所有的編碼轉(zhuǎn)換就只發(fā)生在邊界的地方,JVM和OS的交界處,也就是你的各種輸入輸出流(或者Reader,Writer類)起作用的地方。
話頭扯到這里就必須接著說(shuō)Java的IO系統(tǒng)。
盡管看上去混亂繁雜,但是所有的IO基本上可以分為兩大陣營(yíng):面向字符的Reader啊Wrtier啊,以及面向字節(jié)的輸入輸出流。
下面我來(lái)逐一分解,其實(shí)一點(diǎn)也不難。
面向字符和面向字節(jié)中的所謂“面向”什么,是指這些類在處理輸入輸出的時(shí)候,在哪個(gè)意義上保持一致。如果面向字節(jié),那么這類工作要保證系統(tǒng)中的文件二進(jìn)制內(nèi)容和讀入JVM內(nèi)部的二進(jìn)制內(nèi)容要一致。不能變換任何0和1的順序。因此這是一種非常“忠實(shí)于原著”的做法(偶然間讓我想起郭敬明抄襲莊羽的文章,那家伙,太忠實(shí)于原著了,笑)。
這種輸入輸出方式很適合讀入視頻文件或者音頻文件,或者任何不需要做變換的文件內(nèi)容。
而面向字符的IO是指希望系統(tǒng)中的文件的字符和讀入內(nèi)存的“字符”(注意和字節(jié)的區(qū)別)要一致。例如我們的中文版WindowsXP系統(tǒng)上有一個(gè)GBK的文本文件,其中有一個(gè)“漢”字,這個(gè)字的GBK編碼是0xBABA(而UTF-16編碼是0x6C49),當(dāng)我們使用面向字符的IO把它讀入內(nèi)存并保存在一個(gè)char型變量中時(shí),我希望IO系統(tǒng)不要傻傻的直接把0xBABA放到這個(gè)char型變量中,我甚至都不關(guān)心這個(gè)char型變量具體的二進(jìn)制內(nèi)容到底是多少,我只希望這個(gè)字符讀進(jìn)來(lái)之后仍然是“漢”這個(gè)字。
從這個(gè)意義上也可以看出,面向字符的IO類,也就是Reader和Writer類,實(shí)際上隱式的為我們做了編碼轉(zhuǎn)換,在輸出時(shí),將內(nèi)存中的UTF-16編碼字符使用系統(tǒng)默認(rèn)的編碼方式進(jìn)行了編碼,而在輸入時(shí),將文件系統(tǒng)中已經(jīng)編碼過(guò)的字符使用默認(rèn)編碼方案進(jìn)行了還原。我兩次提到“默認(rèn)”,是說(shuō)Reader和Writer的聰明也僅此而已了,它們只會(huì)使用這個(gè)默認(rèn)的編碼來(lái)做轉(zhuǎn)換,你不能為一個(gè)Reader或者Writer指定轉(zhuǎn)換時(shí)使用的編碼。這也意味著,如果你使用中文版WindowsXP系統(tǒng),而上面存放了一個(gè)UTF-8編碼的文件,當(dāng)你使用Reader類來(lái)讀入的時(shí)候,它會(huì)傻傻的使用GBK來(lái)做轉(zhuǎn)換,轉(zhuǎn)換后的內(nèi)容當(dāng)然驢唇不對(duì)馬嘴!
這種笨,有時(shí)候其實(shí)是一種傻瓜式的功能提供方式,對(duì)大多數(shù)初級(jí)用戶(以及不需要跨平臺(tái)的高級(jí)用戶)來(lái)說(shuō)反而是件好事。
但我們不一樣啦,我們都是國(guó)家棟梁,肩負(fù)著趕英超美的責(zé)任,必須師夷長(zhǎng)技以治夷,所以我們總還要和GBK編碼以外的文件打交道。
說(shuō)了上面這些內(nèi)容,想必聰明的讀者已經(jīng)看出來(lái),所謂編碼轉(zhuǎn)換就是一個(gè)字符與字節(jié)之間的轉(zhuǎn)換,因此Java的IO系統(tǒng)中能夠指定轉(zhuǎn)換編碼的地方,也就在字符與字節(jié)轉(zhuǎn)換的地方,那就是(讀者:InputSteamReader和OutputStreamWriter!作者:太強(qiáng)了,都會(huì)搶答了!)
這兩個(gè)類是字節(jié)流和字符流之間的適配器類,因此他們肩負(fù)著編碼轉(zhuǎn)換的任務(wù)簡(jiǎn)直太自然啦!要注意,實(shí)際上也只能在這兩類實(shí)例化的時(shí)候指定編碼,是不是很好記呢?
下面來(lái)寫(xiě)一段小程序,來(lái)把“漢”字用我們非常崇拜的UTF-8編碼寫(xiě)到文件中!
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);
}

運(yùn)行之后到c盤(pán)下去找utf-8.txt這個(gè)文件,用UltraEdit打開(kāi),使用16進(jìn)制查看,看到了什么?它的值是0xE6B189!(這正是“漢”這個(gè)字的UTF-8編碼)噢耶!(讀者:這,這有什么好高興的……)
下一節(jié)我們來(lái)看看實(shí)現(xiàn)這種操作的其他方式,讀到這里,你已經(jīng)基本上是字符編碼的高手了哦。