原文:http://blog.csdn.net/54powerman/article/details/77575656java
做者:54powerman安全
一直覺得,java中任意unicode字符串,能夠使用任意字符集轉爲byte[]再轉回來,只要不拋出異常就不會丟失數據,事實證實這是錯的。app
通過這個實例,也明白了爲何 getBytes()須要捕獲異常,雖然有時候它也沒有捕獲到異常。測試
言歸正傳,先看一個實例。編碼
用ISO-8859-1中轉UTF-8數據spa
設想一個場景:.net
用戶A,有一個UTF-8編碼的字節流,經過一個接口傳遞給用戶B;code
用戶B並不知道是什麼字符集,他用ISO-8859-1來接收,保存;blog
在必定的處理流程處理後,把這個字節流交給用戶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使用的標準類型,跨平臺無差別。