1. 程式人生 > >字元編碼和Java中的亂碼問題

字元編碼和Java中的亂碼問題

ASCII碼

  在計算機內部,所有的資訊最終都表示為一堆二進位制形式的資料。每一個二進位制位(bit)有0和1兩種狀態,因此八個二進位制位就可以組合出256種狀態,稱為一個位元組(byte),從0000000到11111111。上世紀60年代,美國製定了一套字元編碼,對英語字元與二進位制位之間的關係做了統一規定,稱之為ASCII碼(American Standard Code for Information Interchange)並沿用至今。ASCII碼一共規定了128個字元的編碼,比如空格是32(00100000),大寫的字母A是65(01000001)。這128個符號(包括32個不能打印出來的控制符號),只佔用了一個位元組的後面7位,最前面的1位統一規定為0。   英語用128個符號編碼就夠了,但是其他語言很多都不止128個符號,比如在法語中,字母上方有注音符號,這種字元就無法用ASCII碼錶示。為此,在某些歐洲國家會利用位元組中閒置的最高位編入新的符號,例如法語中的é的編碼為130(二進位制10000010)。這樣一來,這些歐洲國家使用的編碼體系,可以表示最多256個符號。但是這裡又出現了新的問題,不同的國家有不同的字母,因此,哪怕它們都使用256個符號的編碼方式,代表的字母卻不一樣。比如,130在法語編碼中代表了é,在希伯來語編碼中卻代表了字母ג,但是不管怎樣,所有這些編碼方式中,0~127表示的符號是一樣的,不一樣的只是128~255的這一段。   至於亞洲國家的文字,使用的符號就更多了,1994年由中華書局、中國友誼出版公司出版的《中華字海》就收錄了85568個漢字。一個位元組只能表示256種符號肯定是不夠的,就必須使用多個位元組表達一個符號。比如,簡體中文常見的編碼GB2312使用兩個位元組表示一個漢字,所以理論上最多可以表示65536個符號。

Unicode

  世界上存在著多種編碼方式,同一個二進位制數字可以被解釋成不同的符號。因此,要想開啟一個文字檔案,就必須知道它的編碼方式,否則用錯誤的編碼方式解讀,就會出現亂碼。可以想象,如果有一種編碼,將世界上所有的符號都納入其中。每一個符號都給予一個獨一無二的編碼,那麼亂碼問題就會消失,這就是Unicode。Unicode是一個很大的集合,現在的規模可以容納100多萬個符號。每個符號的編碼都不一樣,比如,U+0041表示英語的大寫字母A,U+660A表示漢字”昊”。需要注意的是,Unicode只是一個符號集,它只規定了符號的二進位制程式碼,卻沒有規定這個二進位制程式碼應該如何儲存。 比如,漢字”昊”的Unicode是十六進位制數660A,轉換成二進位制數是0110 0110 0000 1010,也就是說這個符號的表示至少需要2個位元組。表示其他更大的符號,可能需要3個位元組或者4個位元組。這裡就有兩個問題,第一個問題是,如何才能區別Unicode和ASCII?計算機怎麼知道三個位元組表示一個符號,而不是分別表示三個符號呢?第二個問題是,我們已經知道,英文字母只用一個位元組表示就夠了,如果Unicode統一規定,每個符號用三個或四個位元組表示,那麼每個英文字母前都必然有二到三個位元組是0,這對於儲存來說是極大的浪費,文字檔案的大小會因此大出二三倍,這是無法接受的。為此出現了Unicode的多種儲存方式,也就是說有多種不同的二進位制格式可以用來表示Unicode。

UTF-8

  UTF-8就是在網際網路上使用最廣的一種Unicode的實現方式。其他實現方式還包括UTF-16(字元用兩個位元組或四個位元組表示)和UTF-32(字元用四個位元組表示),不過在網際網路上基本不用。UTF-8最大的一個特點,就是它是一種變長的編碼方式。它可以使用1~4個位元組表示一個符號,根據不同的符號而變化位元組長度。UTF-8的編碼規則很簡單,只有二條: 1. 對於單位元組的符號,位元組的第一位設為0,後面7位為這個符號的Unicode碼。因此對於英語字母,UTF-8編碼和ASCII碼是相同的。 2. 對於n位元組的符號(n>1),第一個位元組的前n位都設為1,第n+1位設為0,後面位元組的前兩位一律設為10。剩下的沒有提及的二進位制位,全部為這個符號的Unicode碼。 下表總結了編碼規則,字母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編碼非常簡單。如果一個位元組的第一位是0,則這個位元組單獨就是一個字元;如果第一位是1,則連續有多少個1,就表示當前字元佔用多少個位元組。

Java中的編解碼

I/O操作時的編解碼

  在進行I/O操作時經常會遇到將位元組流轉換成字元流的場景,Java的API提供了InputStreamReader和OutputStreamWriter來解決這樣的問題,而這兩個類的構造器中都可以指定編碼/解碼的方式。

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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

  從JDK 1.4引入了NIO開始,我們可以使用Charset類提供encode和decode方法實現字元陣列和位元組陣列的轉換,程式碼如下所示:

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());
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

字串的編解碼

  Java中的String類提供了用位元組陣列和指定的編碼構造字串物件的操作,同時也提供了將字串按照指定的編碼解碼成位元組陣列的操作,下面我們來做幾個小實驗。

實驗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));
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

**說明:**ISO-8859-1是單位元組編碼,中文“駱昊”的編碼(0x9a86和0x660a)會被轉換成0x3f,而0x3f是ASCII碼中的’?’,所以中文就變成了問號,而且中文字元的編碼資訊已經丟失,再怎麼解碼也沒有機會還原出原來的中文字元了。所以這種現象也稱之為“編碼黑洞”,因為它把不認識的字元給吞噬掉了。很多Java的框架和產品預設都使用了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"));   
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

說明:這種情況在使用瀏覽器的時候也很常見,伺服器傳過來的是中文字元但是瀏覽器的編碼卻設定為ISO-8859-1就會出這種問題。

  如果中文經過了多次編解碼,那麼還有可能遇到一箇中文字元變成多個問號的情況。其實要解決這些編碼問題原則非常簡單,首先如果要表示中文字元就不能使用單位元組編碼,這樣勢必會出現“黑洞”;其次編碼和解碼使用的“碼”應當是一致的。

URL編碼

  URL是統一資源定位符(Universal Resource Locator)的縮寫,是Internet上標準的資源地址。它最初是由全球資訊網和瀏覽器的發明者英國人Tim Berners-Lee發明用來作為全球資訊網的地址,現在已經被W3C編制為Internet標準(RFC 1738)。統一資源定位符的標準格式如下:

協議://伺服器域名或地址:[埠號]/資源路徑/檔名[?查詢引數]
  • 1

  我們試一試在用谷歌搜尋“駱昊”,來看看瀏覽器位址列中的URL到底是什麼樣的。

https://www.google.com.hk/#safe=strict&q=%E9%AA%86%E6%98%8A
  • 1

  URL中允許出現的字元分為保留字元(有特殊含義的字元)與未保留字元,未保留字元包括英文大小寫字母、0-9的數字以及‘-’、 ‘_’、 ‘.’和’~’,保留字元包括 ‘!’、 ‘*’、 ‘’’、 ‘(’、 ‘)’、 ‘;’、 ‘:’、 ‘@’、 ‘&’、 ‘=’、 ‘+’、 ‘$’、 ‘,’、 ‘/’、 ‘?’、 ‘#’、 ‘[’和‘]’。如果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);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

幾種編碼格式的比較

  表示中文可以選擇的編碼方式很多,包括GB2312、GBK、GB18030、UTF-8和UTF-16。UTF-16定義了Unicode字元在計算機中的存取方式,用固定長度的兩個位元組來表示所有的字元,Java中的char型別之所以是兩個位元組就是因為Java使用了UTF-16作為記憶體中字元儲存的格式。UTF-16的編碼效率高,字元與位元組之間的轉換也相對簡單,但是如果在網路上傳輸資料的話會遇到大尾數和小尾數字節順序轉換的問題,因此UTF-8更適合在網路上傳輸資料,而UTF-16更適合在記憶體中使用。UTF-8使用了變長儲存的方式,對ASCII字符采用單位元組儲存,對其他字元可以使用1~6個位元組來表示,編碼效率介於GBK和UTF-16之間,因此開發Java Web應用時,強烈建議使用UTF-8這種編碼方式。

--------------------- 本文來自 駱昊 的CSDN 部落格 ,全文地址請點選:https://blog.csdn.net/jackfrued/article/details/54585693?utm_source=copy