UTF-8與GBK互轉,為什么會亂碼


原文路徑:http://blog.csdn.net/u010234516/article/details/52853214

 

我們知道,計算機存儲數據都是2進制,就是0和1,那么這么多的字符就都需要有自己對應的0和1組成的序列,計算機將需要存儲的字符轉換成它們對應的01序列,然后就可以儲存在電腦里了。

 

比如我們可以定義用8位2進制表示一個字符,“00000000”表示小寫字母“a”,“00000001”表示小寫字母“b”,那么計算機要存儲“ab”的時候,其實在計算機里的存儲的是“0000000000000001”,讀取的時候先讀取前8位,根據對應關系,可以解碼出“a”,再讀取后8位,又可以解碼出“b”,這樣就讀出了當時寫入的“ab”了。而我們定義的這種字符和二進制序列的對應關系,就可以稱之為編碼。我們如果需要將“ab”發送給別人,因為網絡也是基於二進制,所以只要先約定好編碼規則,就可以發送“0000000000000001”,然后對方根據約定的編碼解碼,就可以得到“ab”。現在是互聯網的時代,我們經常需要和其他的計算機進行交互,一套編碼系統還是比較復雜的,所以大家就需要約定統一的編碼,這樣的編碼是大家都約定好的,就不用再去約定編碼規則了~然而,為了滿足各種不同的需求,人們還是制定了很多種編碼,沒有哪一種能全面替代其他編碼,所以現在多種編碼並存。通常這些編碼都被大家所接受和熟知,所以現在不用再通信前商量編碼的對應規則和細節,只需要告訴對方,我采用的是什么通用編碼,彼此就能愉快地通信了。

 

所以亂碼的本質就是:讀取二進制的時候采用的編碼和最初將字符轉換成二進制時的編碼不一致。

 

ps:編碼有動詞含義也有名詞含義,名詞含義就是一套字符和二進制序列之間的轉換規則,動詞含義是使用這種規則將字符轉換成二進制序列。

 

好了,廢話不多,直接上一段代碼:

 

[java] view plain copy

 
 

 

  1. import java.io.UnsupportedEncodingException;  
  2.   
  3. public class EncodingTest {  
  4.     public static void main(String[] args) throws UnsupportedEncodingException {  
  5.         String srcString = "我們是中國人";  
  6.         String utf2GbkString = new String(srcString.getBytes("UTF-8"),"GBK");  
  7.         System.out.println("UTF-8轉換成GBK:"+utf2GbkString);  
  8.         String utf2Gbk2UtfString = new String(utf2GbkString.getBytes("GBK"),"UTF-8");  
  9.         System.out.println("UTF-8轉換成GBK再轉成UTF-8:"+utf2Gbk2UtfString);  
  10.     }  
  11. }  

 

因為UTF-8和GBK是兩套中文支持較好的編碼,所以經常會進行它們之間的轉換,這里就以它們舉例。

以上代碼運行打印出以下內容:

 

UTF-8轉換成GBK:鎴戜滑鏄腑鍥戒漢
UTF-8轉換成GBK再轉成UTF-8:我們是中國人

 

我們看到,將"我們是中國人"以UTF-8編碼轉換成byte數組(byte數組其實就相當於二進制序列了,此過程即編碼),再以GBK編碼和byte數組創建新的字符串(此過程即以GBK編碼去解碼byte數組,得到字符串),就產生亂碼了。

因為編碼采用的UTF-8和解碼采用的GBK不是同一種編碼,所以最后結果亂碼了。

之后再對亂碼使用GBK編碼,還原到解碼前的byte數組,再使用和最初編碼時使用的一致的編碼UTF-8進行解碼,就可得到最初的“我們是中國人”。

這種多余的轉換有時候還是很有用的,比如ftp協議只支持ISO-8859-1編碼,這個時候如果要傳中文,只能先換成ISO-8859-1的亂碼,ftp完成后,再轉回UTF-8就又可以得到正常的中文了。

 

怎么樣?編碼轉換是不是so easy?那該來點正經的了:

 

[java] view plain copy

 
 

 

  1. import java.io.UnsupportedEncodingException;  
  2.   
  3. public class EncodingTest {  
  4.     public static void main(String[] args) throws UnsupportedEncodingException {  
  5.         String srcString = "我們是中國人";  
  6.         String gbk2UtfString = new String(srcString.getBytes("GBK"), "UTF-8");  
  7.         System.out.println("GBK轉換成UTF-8:" + gbk2UtfString);  
  8.         String gbk2Utf2GbkString = new String(gbk2UtfString.getBytes("UTF-8"), "GBK");  
  9.         System.out.println("GBK轉換成UTF-8再轉成GBK:" + gbk2Utf2GbkString);  
  10.     }  
  11. }  

這次我們反過來,先將字符串以GBK編碼再以UTF-8解碼,再以UTF-8編碼,再以GBK解碼。

 

這次的運行結果是:

 

GBK轉換成UTF-8:�������й���
GBK轉換成UTF-8再轉成GBK:錕斤拷錕斤拷錕斤拷錕叫癸拷錕斤拷

 

WTF??萬惡的“錕斤拷”,相信不少人都見過。這里GBK轉成UTF-8亂碼好理解,但是再轉回來怎么變成了“錕斤拷錕斤拷錕斤拷錕叫癸拷錕斤拷”,這似乎不科學。

這其實和UTF-8獨特的編碼方式有關,由於UTF-8需要對unicode字符進行編碼,unicode字符集是一個幾乎支持所有字符的字符集,為了表示這么龐大的字符集,UTF-8可能需要更多的二進制位來表示一個字符,同時為了不致使UTF-8編碼太占存儲空間,根據二八定律,UTF-8采用了一種可變長的編碼方式,即將常用的字符編碼成較短的序列,而不常用的字符用較長的序列表示,這樣讓編碼占用更少存儲空間的同時也保證了對龐大字符集的支持。

正式由於UTF-8采用的這種特別的變長編碼方式,這一點和其他的編碼很不一樣。比如GBK固定用兩個字節來表示漢字,一個字節來表示英文和其他符號。

來測試一下:

[java] view plain copy

 
 

 

  1. import java.io.UnsupportedEncodingException;  
  2.   
  3. public class EncodingTest {  
  4.     public static void main(String[] args) throws UnsupportedEncodingException {  
  5.         String srcString = "我們是中國人";  
  6.         byte[] GbkBytes = srcString.getBytes("GBK");  
  7.         System.out.println("GbkBytes.length:" + GbkBytes.length);  
  8.         byte[] UtfBytes = srcString.getBytes("UTF-8");  
  9.         System.out.println("UtfBytes.length:" + UtfBytes.length);  
  10.         String s;  
  11.         for (int i = 0; i < srcString.length(); i++) {  
  12.             s = Character.valueOf(srcString.charAt(i)).toString();  
  13.             System.out.println(s + ":" + s.getBytes().length);  
  14.         }  
  15.     }  
  16. }  

運行結果為:

 

GbkBytes.length:12
UtfBytes.length:18
我:3
們:3
是:3
中:3
國:3
人:3

 

可以看到使用GBK進行編碼,“我們是中國人”6個漢字占12個字節,而是用UTF-8進行編碼則占了18個字節,其中每個漢字占3個字節(由於是常用漢字,只占3個字節,有的稀有漢字會占四個字節。)

UTF-8編碼的讀取方式也比較不同,需要先讀取第一個字節,然后根據這個字節的值才能判斷這個字節之后還有幾個字節共同參與一個字符的表示。

對於某一個字符的UTF-8編碼,如果只有一個字節則其最高二進制位為0;如果是多字節,其第一個字節從最高位開始,連續的二進制位值為1的個數決定了其編碼的位數,其余各字節均以10開頭。UTF-8最多可用到6個字節。 

如表: 
1字節 0xxxxxxx 
2字節 110xxxxx 10xxxxxx 
3字節 1110xxxx 10xxxxxx 10xxxxxx 
4字節 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx 
5字節 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 
6字節 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 
因此UTF-8中可以用來表示字符編碼的實際位數最多有31位,即上表中x所表示的位。除去那些控制位(每字節開頭的10等),這些x表示的位與UNICODE編碼是一一對應的,位高低順序也相同。 
實際將UNICODE轉換為UTF-8編碼時應先去除高位0,然后根據所剩編碼的位數決定所需最小的UTF-8編碼位數。 
因此那些基本ASCII字符集中的字符(UNICODE兼容ASCII)只需要一個字節的UTF-8編碼(7個二進制位)便可以表示。 

上面一隨便看看就好,只要知道“由於UTF-8的特殊編碼方式,所以有些序列是不可能出現在UTF-8編碼中的”就可以了。

 

所以當我們將由GBK編碼的12個字節試圖用UTF-8解碼時會出現錯誤,由於GBK編碼出了不可能出現在UTF-8編碼中出現的序列,所以當我們試圖用UTF-8去解碼時,經常會遇到這種不可能序列,對於這種不可能序列,UTF-8把它們轉換成某種不可言喻的字符“�”,當這種不可言喻的字符再次以UTF-8進行編碼時,他們已經無法回到最初的樣子了,因為那些是UTF-8編碼不可能編出的序列。然后這個神秘字符再轉換成GBK編碼時就變成了“錕斤拷”。當然,還有很多其他的巧合,可能正好碰到UTF-8中存在的序列,甚至原本不是一個字符的字節,可能是某個字的第二個字節和下一個字的兩個字節,正好被識別成一個UTF-8序列,於是解碼出一個漢字,當然這些在我們看來都是亂碼了,只不過不是“錕斤拷”的樣子。因為不可能序列更普遍存在,所以GBK轉UTF-8再轉GBK時,最常見的便是“錕斤拷”!

 

所以:以非UTF-8編碼編碼出的字節數組,一旦以UTF-8進行解碼,通常這是一條不歸路,再嘗試將解碼出的字符以UTF-8進行編碼,也無法還原之前的字節數組。

相反地,其他的固定長度編碼幾乎都可以順利還原。

 

 

=====================2016/11/15補充==========================

上文中其實有一個東西一直在回避,就是既然所有字符在保存時都需要轉換成二進制,那么java是使用什么編碼來保存字符的呢?這個問題其實我們可以不必深究,因為這對我們是透明的,我們只要假設java使用某種編碼可以表示所有字符。得益於這種透明,我們可以當作java是直接保存字符本身的,就如上文所做的這樣。但是今天面試的時候被問到了,我說這個是對我們透明所以沒有深究。他說雖然是透明的,但是如果弄懂其中的原理還是能加深理解。我馬上想到unicode,因為java要准確地表示所有字符,那么只有unicode能勝任了。這個回答也得到面試官的肯定,還說了一些更細節的。每種編碼都會提供和unicode編碼之間的轉換規則。當我們以字符串直接量new一個String,這個String就是以unicode在內存中存儲的。同樣這也解決了一個讓我疑惑的問題:為什么一個char中既可以存儲一個字母,也可以存儲一個漢字,明明很多編碼如GBK、UTF-8中漢字和字母的長度不一樣。如果java虛擬機使用unicode編碼,那這一切就很好理解了,字母和漢字長度一樣。

 

新增一條結論:java虛擬機中以使用unicode編碼保存字符,任何編碼都提供了和unicode編碼的轉換規則。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM