天天看點

編碼轉換問題

注意iso-8859-1是JAVA網絡傳輸使用的标準字元集,而gb2312是  
  标準中文字元集,當你作出送出表單等需要網絡傳輸的操作的時候,  
  就需要把 iso-8859-1轉換為gb2312字元集顯示,否則如果  
  按浏覽器的gb2312格式來解釋iso-8859-1字元集的話,  
  由于2者不相容, 是以會 是亂碼.           
UTF-8三個位元組代表一個char
iso-8859-1一個位元組代表一個char
GBK兩個位元組代表一個char
           

一直以為java中任意unicode字元串可以使用任意字元集轉為byte[]再轉回來隻要不抛出異常就不會丢失資料事實證明這是錯的。

經過這個執行個體也明白了為什麼 getBytes()需要捕獲異常雖然有時候它也沒有捕獲到異常。

言歸正傳先看一個執行個體。

用ISO-8859-1中轉UTF-8資料

設想一個場景

使用者A有一個UTF-8編碼的位元組流通過一個接口傳遞給使用者B

使用者B并不知道是什麼字元集他用ISO-8859-1來接收儲存

在一定的處理流程處理後把這個位元組流交給使用者C或者交還給使用者A他們都知道這是UTF-8他們解碼得到的資料不會丢失。

下面代碼驗證

public static void main(String[] args) throws Exception {
//這是一個unicode字元串與字元集無關
  String str1 = "使用者";           

System.out.println("unicode字元串"+str1);

//将str轉為UTF-8位元組流 byte[] byteArray1=str1.getBytes("UTF-8");//這個很安全UTF-8不會造成資料丢失

System.out.println(byteArray1.length);//列印6沒毛病

//下面交給另外一個人他不知道這是UTF-8位元組流是以他當做ISO-8859-1處理

//将byteArray1當做一個普通的位元組流按照ISO-8859-1解碼為一個unicode字元串 String str2=new String(byteArray1,"ISO-8859-1");

System.out.println("轉成ISO-8859-1會亂碼"+str2);

//将ISO-8859-1編碼的unicode字元串轉回為byte[] byte[] byteArray2=str2.getBytes("ISO-8859-1");//不會丢失資料

//将位元組流重新交回給使用者A

//重新用UTF-8解碼 String str3=new String(byteArray2,"UTF-8");

System.out.println("資料沒有丢失"+str3); }

輸出

unicode字元串使用者
6
轉成ISO-8859-1會亂碼用户
資料沒有丢失使用者           

用GBK中轉UTF-8資料

重複前面的流程将ISO-8859-1 用GBK替換。

隻把中間一段改掉

//将byteArray1當做一個普通的位元組流按照GBK解碼為一個unicode字元串           
String str2=<span class="hljs-keyword">new</span> String(byteArray1,<span class="hljs-string">"GBK"</span>);

    System.out.println(<span class="hljs-string">"轉成GBK會亂碼"</span>+str2);

    <span class="hljs-comment">//将GBK編碼的unicode字元串轉回為byte[]</span>
    <span class="hljs-keyword">byte</span>[] byteArray2=str2.getBytes(<span class="hljs-string">"GBK"</span>);<span class="hljs-comment">//資料會不會丢失呢</span></code></pre>
                

運作結果

unicode字元串使用者
6
轉成GBK會亂碼鐢ㄦ埛
資料沒有丢失使用者                

好像沒有問題這就是一個誤區。

修改原文字元串重新測試

将兩個漢字 “使用者” 修改為三個漢字 “使用者名” 重新測試。

ISO-8859-1測試結果

unicode字元串使用者名
9
轉成GBK會亂碼用户å
資料沒有丢失使用者名                

GBK 測試結果

unicode字元串使用者名
9
轉成GBK會亂碼鐢ㄦ埛鍚
資料沒有丢失使用者?                

結論出來了

ISO-8859-1 可以作為中間編碼不會導緻資料丢失

GBK 如果漢字數量為偶數不會丢失資料如果漢字數量為奇數必定會丢失資料。

why

為什麼奇數個漢字GBK會出錯

直接對比兩種字元集和奇偶字數的情形

重新封裝一下前面的邏輯寫一段代碼來分析

public static void demo(String str) throws Exception {
  System.out.println("原文" + str);                

byte[] utfByte = str.getBytes("UTF-8"); System.out.print("utf Byte"); printHex(utfByte); String gbk = new String(utfByte, "GBK");//這裡實際上把資料破壞了 System.out.println("to GBK" + gbk);

byte[] gbkByte=gbk.getBytes("GBK"); String utf = new String(gbkByte, "UTF-8"); System.out.print("gbk Byte"); printHex(gbkByte); System.out.println("revert UTF8" + utf); System.out.println("==="); // 如果gbk變成iso-8859-1就沒問題 }

public static void printHex(byte[] byteArray) { StringBuffer sb = new StringBuffer(); for (byte b : byteArray) {

sb.append(Integer.toHexString((b &gt;&gt; <span class="hljs-number">4</span>) &amp; <span class="hljs-number">0xF</span>));
sb.append(Integer.toHexString(b &amp; <span class="hljs-number">0xF</span>));
sb.append(<span class="hljs-string">" "</span>);                     

} System.out.println(sb.toString()); };

public static void main(String[] args) throws Exception { String str1 = "姓名"; String str2 = "使用者名"; demo(str1,"UTF-8","ISO-8859-1"); demo(str2,"UTF-8","ISO-8859-1");

demo(str1,"UTF-8","GBK"); demo(str2,"UTF-8","GBK"); }

輸出結果

原文姓名
UTF-8 Bytee5 a7 93 e5 90 8d
to ISO-8859-1:姓å
ISO-8859-1 Bytee5 a7 93 e5 90 8d
revert UTF-8姓名
===
原文使用者名
UTF-8 Bytee7 94 a8 e6 88 b7 e5 90 8d
to ISO-8859-1:用户å
ISO-8859-1 Bytee7 94 a8 e6 88 b7 e5 90 8d
revert UTF-8使用者名
===
原文姓名
UTF-8 Bytee5 a7 93 e5 90 8d
to GBK:濮撳悕
GBK Bytee5 a7 93 e5 90 8d
revert UTF-8姓名
===
原文使用者名
UTF-8 Bytee7 94 a8 e6 88 b7 e5 90 8d
to GBK:鐢ㄦ埛鍚
GBK Bytee7 94 a8 e6 88 b7 e5 90 3f
revert UTF-8使用者?
===                

為什麼GBK會出錯

前三段都沒問題最後一段奇數個漢字的utf-8位元組流轉成GBK字元串再轉回來前面一切正常最後一個位元組變成了 “0x3f”即”?”

我們使用”使用者名” 三個字來分析它的UTF-8 的位元組流為

[e7 94 a8] [e6 88 b7] [e5 90 8d]

我們按照三個位元組一組分組他被使用者A當做一個整體交給使用者B。

使用者B由于不知道是什麼字元集他當做GBK處理因為GBK是雙位元組編碼如下按照兩兩一組進行分組

[e7 94] [a8 e6] [88 b7] [e5 90] [8d ]

不夠了怎麼辦它把 0x8d當做一個未知字元用一個半角Ascii字元的 “” 代替變成了

[e7 94] [a8 e6] [88 b7] [e5 90] 3f

資料被破壞了。

為什麼 ISO-8859-1 沒問題

因為 ISO-8859-1 是單位元組編碼是以它的分組方案是

[e7] [94] [a8] [e6] [88] [b7] [e5] [90] [8d]

是以中間不做任何操作交回個使用者A的時候資料沒有變化。

關于Unicode編碼

因為UTF-16 區分大小端嚴格講unicode==UTF16BE。

public static void main(String[] args) throws Exception {
  String str="測試";
  printHex(str.getBytes("UNICODE"));
  printHex(str.getBytes("UTF-16LE"));
  printHex(str.getBytes("UTF-16BE"));
}                

運作結果

fe ff 6d 4b 8b d5
4b 6d d5 8b
6d 4b 8b d5                

其中 “fe ff” 為大端消息頭同理小端消息頭為 “ff fe”。

小結

作為中間轉存方案ISO-8859-1 是安全的。

UTF-8 位元組流用GBK字元集中轉是不安全的反過來也是同樣的道理。

byte[] utfByte = str.getBytes("UTF-8");
String gbk = new String(utfByte, "GBK");
這是錯誤的用法雖然在ISO-8859-1時并沒報錯。                

首先byte[] utfByte = str.getBytes("UTF-8"); 執行完成之後utfByte 已經很明确這是utf-8格式的位元組流

然後gbk = new String(utfByte, "GBK") 對utf-8的位元組流使用gbk解碼這是不合規矩的。

就好比一個美國人說一段英語讓一個不懂英文又不會學舌的日本人聽然後傳遞消息給另一個美國人。

為什麼ISO-8859-1 沒問題呢

因為它隻認識一個一個的位元組就相當于是一個錄音機。我管你說的什麼鬼話連篇過去直接播放就可以了。

getBytes() 是會丢失資料的操作而且不一定會抛異常。

unicode是安全的因為他是java使用的标準類型跨平台無差異。