java尾部乱码_再再談java亂碼:GBK和UTF-8互轉尾部亂碼問題分析(續)

本文详细分析了从GBK字节序列通过`new String(b, "UTF-8")`操作可能会导致的数据破坏,尤其是当转换为UTF-8时,可能会出现大量乱码。作者通过代码示例展示了GBK字节码用UTF-8解码时的问题,并解释了UTF-8编码规则和Unicode的关系。最后,讨论了不同JDK版本在处理乱码时的行为差异,指出Java 8在保留有效数据方面进行了优化。" 116690707,10295329,理解SR-IOV:硬件加速虚拟化网络技术,"['虚拟化技术', '硬件辅助', '网络性能优化', 'Linux', 'KVM虚拟化']
摘要由CSDN通过智能技术生成

在《再談java亂碼:GBK和UTF-8互轉尾部亂碼問題分析》我們分析了,如果從一個UTF-8 的字節序列,經過 new String(b,"GBK") 的操作,”可能”(與總字節數有關)會破壞數據。結果可能是,損失最后一個”字”。

反過來呢?可能會很慘,大范圍潰散。。。

GBK字節碼用UTF-8解碼

來看一段代碼:

public static void main(String[] args) throws IOException, ParseException {

String str="中國人";

System.out.println(str);

byte[] b=str.getBytes("GBK");

System.out.println("GBK-8 字節碼長度:"+b.length);

printHex(b);

System.out.println("******");

str=new String(b,"UTF-8");

b=str.getBytes("UTF-8");

printHex(b);

System.out.println("按照通常的經驗,三個漢字的UTF-8長度,應該是9,然而不是。");

System.out.println("UTF-8 字節碼長度:"+b.length);

System.out.println("******");

System.out.println("why?");

b="中國人".getBytes("UTF-8");

System.out.println("三個漢字的UTF-8字節碼應該是:"+b.length);

printHex(b);

}

private static void printHex(byte[] b) {

StringBuilder sb=new StringBuilder();

for(byte t:b) {

sb.append(Integer.toHexString((t & 0xF0)>>4).toUpperCase());

sb.append(Integer.toHexString(t & 0xF).toUpperCase())

.append(" ");

}

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

}

輸出結果:

中國人

GBK-8 字節碼長度:6

D6 D0 B9 FA C8 CB

******

EF BF BD D0 B9 EF BF BD EF BF BD EF BF BD

按照通常的經驗,三個漢字的UTF-8長度,應該是9,然而不是。

UTF-8 字節碼長度:14

******

why?

三個漢字的UTF-8字節碼應該是:9

E4 B8 AD E5 9B BD E4 BA BA

原因在於,str=new String(b,"UTF-8"); 這行代碼破壞了數據,而在此之前的數據是正常的。

UTF-8 的編碼規則

我們通常說,UTF-8字符集的漢字,每一個字占3個字節。我們並沒有說過 UTF-8 字符集的一個字符都是3個字節。

UTF-8是一種變長字節編碼方式,它的長度從1~6個字節都是合法的編碼范圍。

對於某一個字符的UTF-8編碼,如果只有一個字節則其最高二進制位為0;

如果是多字節,其第一個字節從最高位開始,二進制位中連續的1的個數決定了其編碼的位數,其余各字節均以10開頭。

UTF-8最多可用到6個字節。

具體可以參看下表:

utf-8的字節數(byte)

有效數據位(bit)

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

我們來數一下x的數量,也就是每一種編碼規則包含的有效數據位:

utf-8的字節數(byte)

有效數據位(bit)

1

7

2

5+6=11

3

4+6*2=16

4

3+6*3=21

5

2+6*4=26

6

1+6*5=31

那么,如果需要編碼的bit數大於可以編碼的bit數,則該編碼方案無效。

假設需要編碼的數據位為6 bits,那么這個六種方案都可以編碼;如果需要編碼的數據位為27 bits,那么只有6字節方案可以編碼。

但事與願違,拋開浪費空間不說,如果我們把3字節漢字的數據位前面強行置0,讓它以4字節編碼,數據轉換過程還是會破壞,這里留一個疑問。

那么,4字節字符到底是什么?emoji,所謂Emoji就是一種在Unicode位於 \u1F601-\u1F64F 區段的字符。這個顯然超過了目前常用的UTF-8字符集的編碼范圍 \u0000-\uFFFF 。

如 “{(byte)0xF0,(byte)0x9F,(byte)0x98,(byte)0x81}” 表示一個笑臉。

言歸正傳,實際上我們關注的是Unicode和UTF-8之間的關系:

Unicode符號范圍

UTF-8編碼方式

0000 0000-0000 007F

0xxxxxxx

0000 0080-0000 07FF

110xxxxx 10xxxxxx

0000 0800-0000 FFFF

1110xxxx 10xxxxxx 10xxxxxx

0001 0000-0010 FFFF

11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

轉碼實例

根據編碼規則,我們手動來把一個漢字進行一個轉碼,來實際體驗一下:

public static void main(String[] args) throws Exception {

System.out.println("UTF-8:");

printBin("中".getBytes("UTF-8"));

System.out.println("unicode:");

printOctet("中".getBytes("UTF-16BE"));

//上面打印的unicode碼是:01001110 00101101

//要轉為UTF-8 ,我們要知道它占用了幾個數據位

//數一數,去掉高位前面的0,是15個數據位

//查上面的表可以知道,可以使用3字節及以上的編碼方案

//完整的unicode碼被分段為:0100 111000 101101,分別拼接上頭,如下:

byte[] tmpb= {(byte)Integer.parseInt("1110"+"0100",2) //第一個字節是1110xxxx

,(byte)Integer.parseInt("10"+"111000",2)

,(byte)Integer.parseInt("10"+"101101",2)

};

//打印看看,應該沒問題

System.out.println(new String(tmpb,"UTF-8"));

}

解決問題

jdk 1.8 測試

開頭提出了問題,現在就解決問題。

例子中的三個漢字,用UTF-8 轉一次為什么不是意料中的9字節,而是14個字節呢?

我們把代碼改一下,打印一下二進制。

public static void main(String[] args) throws Exception {

String str = "中國人";

byte[] b = str.getBytes("GBK");

System.out.println(b.length);

printHex(b);

printOctet(b);//就加了這一行

str = new String(b, "UTF-8");

b = str.getBytes("UTF-8");

System.out.println(b.length);

printHex(b);

}

private static void printHex(byte[] b) {

StringBuilder sb = new StringBuilder();

for (byte t : b) {

sb.append(Integer.toHexString((t & 0xF0) >> 4).toUpperCase());

sb.append(Integer.toHexString(t & 0xF).toUpperCase()).append(" ");

}

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

}

private static void printOctet(byte[] b) {

StringBuilder sb = new StringBuilder();

for (byte t : b) {

sb.append(String.format("%08d", Integer.parseInt(Integer.toBinaryString(t & 0xFF)))).append(" ");

}

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

}

輸出結果:

6

D6 D0 B9 FA C8 CB

11010110 11010000 10111001 11111010 11001000 11001011

14

EF BF BD D0 B9 EF BF BD EF BF BD EF BF BD

來看一下 str = new String(b, "UTF-8"); 這一行到底干了什么事情?

原始的byte[]為:11010110 11010000 10111001 11111010 11001000 11001011

首先讀取第一個字節,11010110,根據UTF-8 編碼規則,因為110開頭,編碼器認為這是一個雙字節的字,它會去取第二個字節,而且要求第二個字節必須是10開頭。這時它發現錯了,因為,他會用 "EF BF BD" 三個字節替換第一個字節,轉成二進制,就是第二段字節流的:“11101111 10111111 10111101”。

"EF BF BD" 是什么?前文已經說過,就是一個標准占位符。

那么,第二個字節它已經拿出來了,根據規則,因為110開頭,編碼器還是當做一個雙字節字處理,再取第三個字節,是10開頭,符合規則,當做雙字節處理,正常。因此,直接把 D0 B9 拼接到新的字節流里,現在新的字節流變成了:[EF BF BD] [D0 B9]

第四個字節,11111010 以111110 開頭,編碼器認為這是一個5字節編碼的UTF-8字,后面至少需要4個后續字節,明顯不夠了。因此,再拼接一個 "EF BF BD" ,新的字節流變成了:[EF BF BD] [D0 B9] [EF BF BD]

依次處理第五、第六個字節,同樣再次拼接了兩個"EF BF BD" ,最終的字節流是:[EF BF BD] [D0 B9] [EF BF BD] [EF BF BD] [EF BF BD]

14個字節。

jdk 1.6/1.7

如果使用 jdk 1.6 和 1.7 來運行用例,結論不同了,最終是8個字節:

中國人

GBK-8 字節碼長度:6

D6 D0 B9 FA C8 CB

******

EF BF BD D0 B9 EF BF BD

按照通常的經驗,三個漢字的UTF-8長度,應該是9,然而不是。

UTF-8 字節碼長度:8

從打印的日志來看,原字節碼,前三個字節的分析沒有問題。問題在於后面的三個字節,遇到錯誤的字節時,編碼器直接用三位的占位符替換了錯誤的三個字節。

jdk 版本的影響

編碼器的源碼暫時沒找到,先從表面上來看一下他們不同的編碼規則的不同。

先看一個例子:

String str="中國86";

System.out.println(str);

byte[] b=str.getBytes("GBK");

str=new String(b,"UTF-8");

System.out.println(str);

輸出結果:

比如用 “中國86" 來測試,java8,打印是這樣的:

�й�86

而 java6、7打印是這樣的:

�й�

如此看來,jdk6、7太暴力,發現一個異常字節,直接忽略后續2個字節,當做一個占位符,哪怕你后面兩個字節 0x38 0x36 是可識別的ascii碼。

因此jdk6、7的破壞性更強,java8的規則是優化了的結果,盡可能保留了有效數據,這也是unicode中占位符的初衷。

Since the replacement is the same for all errors this makes it impossible to recover the original character.

小結

先回顧一下前文的結論:

對於任意字節流,使用ISO-8859-1 轉為字符串再轉回來,是安全的;使用GBK和UTF-8可能會破壞數據。

現在擴展一下,使用GBK可能會破壞數據,損失最后一個字;如果使用UTF-8 可能損失大部分的字。

但這絕不是說UTF-8 是不好的,而是在這個亂碼問題出現的時候,UTF-8是最慘烈的。實際上,UTF-8 尤其是動態長度的編碼方案,無疑是最經濟的。而且,4字節字符的出現,雙字節編碼方案,完全無法解決,唯UTF-8才是較好的選擇(utf-8mb4)。

參考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值