再談java亂碼:GBK和UTF-8互轉尾部亂碼問題分析

我在通過Java調用aapt來獲取apk的應用名稱時,發現如果應用名稱爲中文,則會出現亂碼,例如:

'VlogStar鍗$偣瑙嗛蹇壀杈戣蔣浠?'

怎麼辦呢?當然是轉碼了:

 

applicationLabelMap.put(language, new String(label.getBytes(MyDocumentManager.ENCODING_CN), MyDocumentManager.ENCODING));

問題似乎解決了,以GBK解碼,重新以UTF8編碼,中文正常顯示了。但中文並不總是正常顯示,這與字符數有關,當字符數爲奇數時,最後一箇中文字符顯示不正常:

'VlogStar鍗$偣瑙嗛蹇壀杈戣蔣浠?' > 'VlogStar卡點視頻快剪輯軟??'

爲什麼會這樣呢?因爲字符串在轉碼過程中發生了錯誤,雖然沒有任何異常產生,但數據已經發生了損壞。

針對我這一問題的解決辦法,就是在讀取aapt的輸出流的時候,指定編碼:

	@Override
	public final void run()
	{
		if(null != this.is)
		{
			this.outputList.clear();
			InputStreamReader ir = null;
			BufferedReader br = null;
			
			try
			{
				ir = new InputStreamReader(this.is, this.encoding);
				br = new BufferedReader(ir);
				String text = null;
				
				while(null != (text = br.readLine()))
				{
					this.append(text);
				}
			}
			catch(IOException e)
			{
				e.printStackTrace();
			}
			finally
			{
				this.close(br);
				this.close(ir);
				this.close(this.is);
			}
		}
	}

 

至於GBK和UTF-8互轉尾部亂碼問題,可以參考:

原文地址:https://blog.csdn.net/54powerman/article/details/77575656

 

一直以爲,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=new String(byteArray1,"GBK");

        System.out.println("轉成GBK會亂碼:"+str2);

        //將GBK編碼的unicode字符串轉回爲byte[]
        byte[] byteArray2=str2.getBytes("GBK");//數據會不會丟失呢?

運行結果:

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 >> 4) & 0xF));
    sb.append(Integer.toHexString(b & 0xF));
    sb.append(" ");
  }
  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 Byte:e5 a7 93 e5 90 8d
to ISO-8859-1:姓å
ISO-8859-1 Byte:e5 a7 93 e5 90 8d
revert UTF-8:姓名
===
原文:用戶名
UTF-8 Byte:e7 94 a8 e6 88 b7 e5 90 8d
to ISO-8859-1:用户å
ISO-8859-1 Byte:e7 94 a8 e6 88 b7 e5 90 8d
revert UTF-8:用戶名
===
原文:姓名
UTF-8 Byte:e5 a7 93 e5 90 8d
to GBK:濮撳悕
GBK Byte:e5 a7 93 e5 90 8d
revert UTF-8:姓名
===
原文:用戶名
UTF-8 Byte:e7 94 a8 e6 88 b7 e5 90 8d
to GBK:鐢ㄦ埛鍚�
GBK Byte:e7 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使用的標準類型,跨平臺無差異。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章