在工作中遇到了标题所述的问题,当一个字节数组编码成字符串后再获得字符串的字节数组,发现会和一开始的字节序列不同。上网查询了一番,发现Stack Overflow上有同样的问题,现在就来分析一下为什么会出现这种情况。
byte[] bytes1 = {-1, 127, 0, 38, 97, 104, 55, 110, 50, -24, -48, 59, -20, -6, 64, 1, 4, 107, 56, 54 };
String msg = new String( bytes1, "UTF-8" );
byte[] bytes2 = msg.getBytes( "UTF-8" );
for( byte curr : bytes1 ) {
System.out.print( curr );
System.out.print( ", " );
}
System.out.println();
for( byte curr : bytes2 ) {
System.out.print( curr );
System.out.print( ", " );
}
//output
-1, 127, 0, 38, 97, 104, 55, 110, 50, -24, -48, 59, -20, -6, 64, 1, 4, 107, 56, 54,
-17, -65, -67, 127, 0, 38, 97, 104, 55, 110, 50, -17, -65, -67, -17, -65, -67, 59, -17, -65, -67, -17, -65, -67, 64, 1, 4, 107, 56, 54,
在分析这个问题前,我们需要了解一下Unicode编码的知识。
需要注意的是,Unicode 只是一个符号集,它只规定了符号的二进制代码,却没有规定这个二进制代码应该如何存储。
比如,汉字严的 Unicode 是十六进制数4E25,转换成二进制数足足有15位(100111000100101),也就是说,这个符号的表示至少需要2个字节。表示其他更大的符号,可能需要3个字节或者4个字节,甚至更多。
简单来说,Unicode就是定义了一个符号对应的二进制码。utf-8,utf-16等代表不同的实现方式。
下面我们看一下utf-8如何实现Unicode
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
拿上面的严
来说,二进制码是100111000100101
,选择上表的其中一行将x
替换为二进制码,选择哪一行呢,第一行只有7位可以填,第二行只有11位可以填,所以都不行。第三行有16个,比我们目标值多一位,我们从右往左填,高位补0即可。结果为11100100 10111000 10100101
。
这时候我们观察一下上面的输出,所有的正数都能正确输出,所有的负数都会变成-17, -65, -67
。
导致这个问题原因在于utf-8是有编码规则的,如上图所示,当java从字节数组里二进制转str时,发现不符合utf-8规定的格式,该字符就会被替代成�
(可以参考JDK文档)。�
的Unicode码为'\ufffd'
,对应的字节就是-17, -65, -67
。
对于上面的案例我们可以直接使用utf-16bl(大头方式)或者utf-16le(小头方式)编码,直接用utf-16编码会多两个字节FE FF
表示是大头方式,有兴趣可以自行查询了解。
参考链接