java中文GBK和UTF-8編碼轉換亂碼的分析


 

原文:http://blog.csdn.net/54powerman/article/details/77575656

作者:54powerman

一直以為,java中任意unicode字符串,可以使用任意字符集轉為byte[]再轉回來,只要不拋出異常就不會丟失數據,事實證明這是錯的。

經過這個實例,也明白了為什么 getBytes()需要捕獲異常,雖然有時候它也沒有捕獲到異常。

言歸正傳,先看一個實例。

用ISO-8859-1中轉UTF-8數據

設想一個場景:

用戶A,有一個UTF-8編碼的字節流,通過一個接口傳遞給用戶B;

用戶B並不知道是什么字符集,他用ISO-8859-1來接收,保存;

在一定的處理流程處理后,把這個字節流交給用戶C或者交還給用戶A,他們都知道這是UTF-8,他們解碼得到的數據,不會丟失。

下面代碼驗證:

  1 public static void main(String[] args) throws Exception {
  2  	  //這是一個unicode字符串,與字符集無關
  3  	  String str1 = "用戶";
  4 
  5  	  System.out.println("unicode字符串:"+str1);
  6 
  7  	  //將str轉為UTF-8字節流
  8  	  byte[] byteArray1=str1.getBytes("UTF-8");//這個很安全,UTF-8不會造成數據丟失
  9 
 10  	  System.out.println(byteArray1.length);//打印6,沒毛病
 11 
 12  	  //下面交給另外一個人,他不知道這是UTF-8字節流,因此他當做ISO-8859-1處理
 13 
 14  	  //將byteArray1當做一個普通的字節流,按照ISO-8859-1解碼為一個unicode字符串
 15  	  String str2=new String(byteArray1,"ISO-8859-1");
 16 
 17  	  System.out.println("轉成ISO-8859-1會亂碼:"+str2);
 18 
 19  	  //將ISO-8859-1編碼的unicode字符串轉回為byte[]
 20  	  byte[] byteArray2=str2.getBytes("ISO-8859-1");//不會丟失數據
 21 
 22  	  //將字節流重新交回給用戶A
 23 
 24  	  //重新用UTF-8解碼
 25  	  String str3=new String(byteArray2,"UTF-8");
 26 
 27  	  System.out.println("數據沒有丟失:"+str3);
 28  	}
 29 輸出:
 30 
 31  	unicode字符串:用戶
 32  	6
 33  	轉成ISO-8859-1會亂碼:用户
 34  	數據沒有丟失:用戶

用GBK中轉UTF-8數據

重復前面的流程,將ISO-8859-1 用GBK替換。

只把中間一段改掉:

  1 //將byteArray1當做一個普通的字節流,按照GBK解碼為一個unicode字符串
  2  	    String str2=new String(byteArray1,"GBK");
  3 
  4  	    System.out.println("轉成GBK會亂碼:"+str2);
  5 
  6  	    //將GBK編碼的unicode字符串轉回為byte[]
  7  	    byte[] byteArray2=str2.getBytes("GBK");//數據會不會丟失呢?
  8 運行結果:
  9 
 10  	unicode字符串:用戶
 11  	6
 12  	轉成GBK會亂碼:鐢ㄦ埛
 13  	數據沒有丟失:用戶

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

修改原文字符串重新測試

將兩個漢字 “用戶” 修改為三個漢字 “用戶名” 重新測試。

ISO-8859-1測試結果:

  1 unicode字符串:用戶名
  2  	9
  3  	轉成GBK會亂碼:用户名
  4  	數據沒有丟失:用戶名
  5 GBK 測試結果:
  6 
  7  	unicode字符串:用戶名
  8  	9
  9  	轉成GBK會亂碼:鐢ㄦ埛鍚�
 10  	數據沒有丟失:用戶�?

結論出來了

ISO-8859-1 可以作為中間編碼,不會導致數據丟失;

GBK 如果漢字數量為偶數,不會丟失數據,如果漢字數量為奇數,必定會丟失數據。

why?

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

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

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

  1 public static void demo(String str) throws Exception {
  2  	  System.out.println("原文:" + str);
  3 
  4  	  byte[] utfByte = str.getBytes("UTF-8");
  5  	  System.out.print("utf Byte:");
  6  	  printHex(utfByte);
  7  	  String gbk = new String(utfByte, "GBK");//這里實際上把數據破壞了
  8  	  System.out.println("to GBK:" + gbk);
  9 
 10  	  byte[] gbkByte=gbk.getBytes("GBK");
 11  	  String utf = new String(gbkByte, "UTF-8");
 12  	  System.out.print("gbk Byte:");
 13  	  printHex(gbkByte);
 14  	  System.out.println("revert UTF8:" + utf);
 15  	  System.out.println("===");
 16  	//      如果gbk變成iso-8859-1就沒問題
 17  	}
 18 
 19  	public static void printHex(byte[] byteArray) {
 20  	  StringBuffer sb = new StringBuffer();
 21  	  for (byte b : byteArray) {
 22  	    sb.append(Integer.toHexString((b >> 4) & 0xF));
 23  	    sb.append(Integer.toHexString(b & 0xF));
 24  	    sb.append("");
 25  	  }
 26  	  System.out.println(sb.toString());
 27  	};
 28 
 29  	public static void main(String[] args) throws Exception {
 30  	  String str1 = "姓名";
 31  	  String str2 = "用戶名";
 32  	  demo(str1,"UTF-8","ISO-8859-1");
 33  	  demo(str2,"UTF-8","ISO-8859-1");
 34 
 35  	  demo(str1,"UTF-8","GBK");
 36  	  demo(str2,"UTF-8","GBK");
 37  	}
 38 輸出結果:
 39 
 40  	原文:姓名
 41  	UTF-8 Byte:e5 a7 93 e5 90 8d
 42  	to ISO-8859-1:姓名
 43  	ISO-8859-1 Byte:e5 a7 93 e5 90 8d
 44  	revert UTF-8:姓名
 45  	===
 46  	原文:用戶名
 47  	UTF-8 Byte:e7 94 a8 e6 88 b7 e5 90 8d
 48  	to ISO-8859-1:用户名
 49  	ISO-8859-1 Byte:e7 94 a8 e6 88 b7 e5 90 8d
 50  	revert UTF-8:用戶名
 51  	===
 52  	原文:姓名
 53  	UTF-8 Byte:e5 a7 93 e5 90 8d
 54  	to GBK:濮撳悕
 55  	GBK Byte:e5 a7 93 e5 90 8d
 56  	revert UTF-8:姓名
 57  	===
 58  	原文:用戶名
 59  	UTF-8 Byte:e7 94 a8 e6 88 b7 e5 90 8d
 60  	to GBK:鐢ㄦ埛鍚�
 61  	GBK Byte:e7 94 a8 e6 88 b7 e5 90 3f
 62  	revert UTF-8:用戶�?
 63  	===

為什么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。

  1 public static void main(String[] args) throws Exception {
  2  	  String str="測試";
  3  	  printHex(str.getBytes("UNICODE"));
  4  	  printHex(str.getBytes("UTF-16LE"));
  5  	  printHex(str.getBytes("UTF-16BE"));
  6  	}
  7 運行結果:
  8 
  9  	fe ff 6d 4b 8b d5
 10  	4b 6d d5 8b
 11  	6d 4b 8b d5

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

小結

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

UTF-8 字節流,用GBK字符集中轉是不安全的;反過來也是同樣的道理。

  1 byte[] utfByte = str.getBytes("UTF-8");
  2  	String gbk = new String(utfByte, "GBK");
  3  	這是錯誤的用法,雖然在ISO-8859-1時並沒報錯。
  4 
  5  	首先,byte[] utfByte = str.getBytes("UTF-8");
  6  	執行完成之后,utfByte 已經很明確,這是utf-8格式的字節流;
  7 
  8  	然后,gbk = new String(utfByte, "GBK"),
  9  	對utf-8的字節流使用gbk解碼,這是不合規矩的。
 10 
 11  	就好比一個美國人說一段英語,讓一個不懂英文又不會學舌的日本人聽,然后傳遞消息給另一個美國人。
 12 
 13  	為什么ISO-8859-1 沒問題呢?
 14 
 15  	因為它只認識一個一個的字節,就相當於是一個錄音機。我管你說的什么鬼話連篇,過去直接播放就可以了。
 16 getBytes() 是會丟失數據的操作,而且不一定會拋異常。
 17 
 18 unicode是安全的,因為他是java使用的標准類型,跨平台無差異。


免責聲明!

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



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