ASCII碼
在計算機內(nèi)部授嘀,所有的信息最終都表示為一堆二進制形式的數(shù)據(jù)。每一個二進制位(bit)有0和1兩種狀態(tài)给郊,因此八個二進制位就可以組合出256種狀態(tài)菩浙,稱為一個字節(jié)(byte),從0000000到11111111睬愤。上世紀60年代片仿,美國制定了一套字符編碼,對英語字符與二進制位之間的關(guān)系做了統(tǒng)一規(guī)定尤辱,稱之為ASCII碼(American Standard Code for Information Interchange)并沿用至今砂豌。ASCII碼一共規(guī)定了128個字符的編碼厢岂,比如空格是32(00100000),大寫的字母A是65(01000001)阳距。這128個符號(包括32個不能打印出來的控制符號)塔粒,只占用了一個字節(jié)的后面7位,最前面的1位統(tǒng)一規(guī)定為0筐摘。
??英語用128個符號編碼就夠了卒茬,但是其他語言很多都不止128個符號,比如在法語中蓄拣,字母上方有注音符號扬虚,這種字符就無法用ASCII碼表示。為此球恤,在某些歐洲國家會利用字節(jié)中閑置的最高位編入新的符號辜昵,例如法語中的é的編碼為130(二進制10000010)。這樣一來咽斧,這些歐洲國家使用的編碼體系堪置,可以表示最多256個符號。但是這里又出現(xiàn)了新的問題张惹,不同的國家有不同的字母舀锨,因此,哪怕它們都使用256個符號的編碼方式宛逗,代表的字母卻不一樣坎匿。比如,130在法語編碼中代表了é雷激,在希伯來語編碼中卻代表了字母?替蔬,但是不管怎樣,所有這些編碼方式中屎暇,0127表示的符號是一樣的承桥,不一樣的只是128255的這一段。
??至于亞洲國家的文字根悼,使用的符號就更多了凶异,1994年由中華書局、中國友誼出版公司出版的《中華字杭费玻》就收錄了85568個漢字剩彬。一個字節(jié)只能表示256種符號肯定是不夠的,就必須使用多個字節(jié)表達一個符號矿卑。比如襟衰,簡體中文常見的編碼GB2312使用兩個字節(jié)表示一個漢字,所以理論上最多可以表示65536個符號。
Unicode
世界上存在著多種編碼方式瀑晒,同一個二進制數(shù)字可以被解釋成不同的符號绍坝。因此,要想打開一個文本文件苔悦,就必須知道它的編碼方式轩褐,否則用錯誤的編碼方式解讀,就會出現(xiàn)亂碼玖详“呀椋可以想象,如果有一種編碼蟋座,將世界上所有的符號都納入其中拗踢。每一個符號都給予一個獨一無二的編碼,那么亂碼問題就會消失向臀,這就是Unicode巢墅。Unicode是一個很大的集合,現(xiàn)在的規(guī)娜颍可以容納100多萬個符號君纫。每個符號的編碼都不一樣,比如芹彬,U+0041表示英語的大寫字母A蓄髓,U+660A表示漢字"昊"。需要注意的是舒帮,Unicode只是一個符號集会喝,它只規(guī)定了符號的二進制代碼,卻沒有規(guī)定這個二進制代碼應(yīng)該如何存儲玩郊。
比如肢执,漢字"昊"的Unicode是十六進制數(shù)660A,轉(zhuǎn)換成二進制數(shù)是0110 0110 0000 1010瓦宜,也就是說這個符號的表示至少需要2個字節(jié)。表示其他更大的符號岭妖,可能需要3個字節(jié)或者4個字節(jié)临庇。這里就有兩個問題,第一個問題是昵慌,如何才能區(qū)別Unicode和ASCII假夺?計算機怎么知道三個字節(jié)表示一個符號,而不是分別表示三個符號呢斋攀?第二個問題是已卷,我們已經(jīng)知道,英文字母只用一個字節(jié)表示就夠了淳蔼,如果Unicode統(tǒng)一規(guī)定侧蘸,每個符號用三個或四個字節(jié)表示裁眯,那么每個英文字母前都必然有二到三個字節(jié)是0,這對于存儲來說是極大的浪費讳癌,文本文件的大小會因此大出二三倍穿稳,這是無法接受的。為此出現(xiàn)了Unicode的多種存儲方式晌坤,也就是說有多種不同的二進制格式可以用來表示Unicode逢艘。
UTF-8
UTF-8就是在互聯(lián)網(wǎng)上使用最廣的一種Unicode的實現(xiàn)方式。其他實現(xiàn)方式還包括UTF-16(字符用兩個字節(jié)或四個字節(jié)表示)和UTF-32(字符用四個字節(jié)表示)骤菠,不過在互聯(lián)網(wǎng)上基本不用它改。UTF-8最大的一個特點,就是它是一種變長的編碼方式商乎。它可以使用1~4個字節(jié)表示一個符號央拖,根據(jù)不同的符號而變化字節(jié)長度。UTF-8的編碼規(guī)則很簡單截亦,只有二條:
- 對于單字節(jié)的符號爬泥,字節(jié)的第一位設(shè)為0,后面7位為這個符號的Unicode碼崩瓤。因此對于英語字母袍啡,UTF-8編碼和ASCII碼是相同的。
- 對于n字節(jié)的符號(n>1)却桶,第一個字節(jié)的前n位都設(shè)為1境输,第n+1位設(shè)為0,后面字節(jié)的前兩位一律設(shè)為10颖系。剩下的沒有提及的二進制位嗅剖,全部為這個符號的Unicode碼。
下表總結(jié)了編碼規(guī)則嘁扼,字母x表示可用編碼的位信粮。
Unicode符號范圍 | UTF-8編碼方式 |
---|---|
0000 0000-0000 007F | 0xxxxxxx |
0000 0080-0000 07FF | 110xxxxx 10xxxxxx |
0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx |
0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
說明:解讀UTF-8編碼非常簡單。如果一個字節(jié)的第一位是0趁啸,則這個字節(jié)單獨就是一個字符强缘;如果第一位是1,則連續(xù)有多少個1不傅,就表示當前字符占用多少個字節(jié)旅掂。
Java中的編解碼
I/O操作時的編解碼
在進行I/O操作時經(jīng)常會遇到將字節(jié)流轉(zhuǎn)換成字符流的場景,Java的API提供了InputStreamReader和OutputStreamWriter來解決這樣的問題访娶,而這兩個類的構(gòu)造器中都可以指定編碼/解碼的方式商虐。
InputStreamReader(InputStream in) // 使用默認的字符集
InputStreamReader(InputStream in, String charsetName) throws UnsupportedEncodingException
InputStreamReader(InputStream in, Charset cs)
InputStreamReader(InputStream in, CharsetDecoder dec)
OutputStreamWriter(OutputStream out) // 使用默認的字符集
OutputStreamWriter(OutputStream out, String charsetName) throws UnsupportedEncodingException
OutputStreamWriter(OutputStream out, Charset cs)
OutputStreamWriter(OutputStream out, CharsetEncoder enc)
從JDK 1.4引入了NIO開始,我們可以使用Charset類提供encode和decode方法實現(xiàn)字符數(shù)組和字節(jié)數(shù)組的轉(zhuǎn)換,代碼如下所示:
Charset cs = Charset.forName("utf-8");
String str = "駱昊";
ByteBuffer buffer1 = cs.encode(str);
// 駱 昊
// e9 aa 86 e6 98 8a
// 11101001 10101010 10000110 11100110 10011000 10001010
for (int index = 0; index < buffer1.limit(); index += 1) {
System.out.print(Integer.toHexString(buffer1.get(index) & 0xff) + " ");
}
System.out.println();
CharBuffer buffer2 = cs.decode(buffer1);
// 駱昊
System.out.println(buffer2.toString());
字符串的編解碼
Java中的String類提供了用字節(jié)數(shù)組和指定的編碼構(gòu)造字符串對象的操作秘车,同時也提供了將字符串按照指定的編碼解碼成字節(jié)數(shù)組的操作典勇,下面我們來做幾個小實驗。
實驗1:中文變成'?'鲫尊。
public static void main(String[] args) throws UnsupportedEncodingException {
String str = "hello, 駱昊";
byte[] buffer = str.getBytes("iso-8859-1");
// hello, ??
System.out.println(new String(buffer));
}
說明:ISO-8859-1是單字節(jié)編碼痴柔,中文“駱昊”的編碼(0x9a86和0x660a)會被轉(zhuǎn)換成0x3f疫向,而0x3f是ASCII碼中的'?'咳蔚,所以中文就變成了問號,而且中文字符的編碼信息已經(jīng)丟失搔驼,再怎么解碼也沒有機會還原出原來的中文字符了谈火。所以這種現(xiàn)象也稱之為“編碼黑洞”,因為它把不認識的字符給吞噬掉了舌涨。很多Java的框架和產(chǎn)品默認都使用了ISO-8859-1糯耍,所以這個問題很常見。
實驗2:中文變成看不懂的字符囊嘉。
public static void main(String[] args) throws UnsupportedEncodingException {
String str = "hello, 駱昊";
byte[] buffer = str.getBytes("gbk");
// hello, ??ê?
System.out.println(new String(buffer, "iso-8859-1"));
}
說明:這種情況在使用瀏覽器的時候也很常見温技,服務(wù)器傳過來的是中文字符但是瀏覽器的編碼卻設(shè)置為ISO-8859-1就會出這種問題。
如果中文經(jīng)過了多次編解碼扭粱,那么還有可能遇到一個中文字符變成多個問號的情況舵鳞。其實要解決這些編碼問題原則非常簡單,首先如果要表示中文字符就不能使用單字節(jié)編碼琢蛤,這樣勢必會出現(xiàn)“黑洞”蜓堕;其次編碼和解碼使用的“碼”應(yīng)當是一致的。
URL編碼
URL是統(tǒng)一資源定位符(Universal Resource Locator)的縮寫博其,是Internet上標準的資源地址套才。它最初是由萬維網(wǎng)和瀏覽器的發(fā)明者英國人Tim Berners-Lee發(fā)明用來作為萬維網(wǎng)的地址,現(xiàn)在已經(jīng)被W3C編制為Internet標準(RFC 1738)慕淡。統(tǒng)一資源定位符的標準格式如下:
協(xié)議://服務(wù)器域名或地址:[端口號]/資源路徑/文件名[?查詢參數(shù)]
我們試一試在用谷歌搜索“駱昊”背伴,來看看瀏覽器地址欄中的URL到底是什么樣的。
https://www.google.com.hk/#safe=strict&q=%E9%AA%86%E6%98%8A
URL中允許出現(xiàn)的字符分為保留字符(有特殊含義的字符)與未保留字符峰髓,未保留字符包括英文大小寫字母傻寂、0-9的數(shù)字以及‘-’、 ‘_’儿普、 ‘.’和'~'崎逃,保留字符包括 ‘!’掷倔、 ‘*’眉孩、 ‘'’、 ‘(’、 ‘)’浪汪、 ‘;’巴柿、 ‘:’、 ‘@’死遭、 ‘&’广恢、 ‘=’、 ‘+’呀潭、 ‘$’钉迷、 ‘,’、 ‘/’钠署、 ‘?’糠聪、 ‘#’、 ‘[’和‘]’谐鼎。如果URL中需要用到保留字符或者非URL允許的字符則需要使用百分號編碼舰蟆,例如:‘=’要處理成‘%3D’、‘+’要處理成‘%2B’狸棍、而上面要搜索的‘駱’和‘昊’兩個中文字符被處理成了百分號編碼的‘%E9%AA%86’和‘%E6%98%8A’身害。
Java中要將URL中的非URL允許字符處理成百分號編碼有非常簡單的辦法,就是使用URLEncoder類的encode方法草戈,代碼如下所示塌鸯。
public static void main(String[] args) throws UnsupportedEncodingException {
String urlStr = "Java 駱昊";
String encodedUrlStr = URLEncoder.encode(urlStr, "utf-8");
// Java+%E9%AA%86%E6%98%8A
System.out.println(encodedUrlStr);
}
幾種編碼格式的比較
表示中文可以選擇的編碼方式很多,包括GB2312猾瘸、GBK界赔、GB18030、UTF-8和UTF-16牵触。UTF-16定義了Unicode字符在計算機中的存取方式淮悼,用固定長度的兩個字節(jié)來表示所有的字符,Java中的char類型之所以是兩個字節(jié)就是因為Java使用了UTF-16作為內(nèi)存中字符存儲的格式揽思。UTF-16的編碼效率高袜腥,字符與字節(jié)之間的轉(zhuǎn)換也相對簡單,但是如果在網(wǎng)絡(luò)上傳輸數(shù)據(jù)的話會遇到大尾數(shù)和小尾數(shù)字節(jié)順序轉(zhuǎn)換的問題钉汗,因此UTF-8更適合在網(wǎng)絡(luò)上傳輸數(shù)據(jù)羹令,而UTF-16更適合在內(nèi)存中使用。UTF-8使用了變長存儲的方式损痰,對ASCII字符采用單字節(jié)存儲福侈,對其他字符可以使用1~6個字節(jié)來表示,編碼效率介于GBK和UTF-16之間卢未,因此開發(fā)Java Web應(yīng)用時肪凛,強烈建議使用UTF-8這種編碼方式堰汉。