对于程序员来说utf-8编码那真是很常见的了,但试问有多少人知道utf-8内在的原理?下面我尽量使用最短的时间让你了解utf-8。这里不讨论编码历史的发展,那对于现在的你可能毫无意义。假定你对二进制有所了解,并且理解无符号和有符号二进制。
简单的说utf-8就是一张数字和符号的映射表。那么解码的过程就是把二进制串解析成数字,然后从这张表中找到对应的字符。反之就是编码的过程。这个数字是无符号二进制。
那么问题随之就来了,怎么得到这个数字?要回答这个问题就得了解utf-8的结构了,utf-8编码可能占用1个字节也可能占用多个字节,但就目前规定最多占4个字节,所以说UTF-8编码为变长编码(科普:java中汉字的utf-8占用3个字节)。utf-8结构如下:
占用字节数 | 第1个字节 | 第2个字节 | 第3个字节 | 第4个字节 | 16进制数字范围 |
---|---|---|---|---|---|
1 | 0xxxxxxx | 0000到007F | |||
2 | 110xxxxx | 10xxxxxx | 0080 到07FF | ||
3 | 1110xxxx | 10xxxxxx | 10xxxxxx | 0800到FFFF | |
4 | 11110xxx | 10xxxxxx | 10xxxxxx | 10xxxxxx | 10000到1FFFFF |
从这张结构图中我们就可以总结出找到数字的方法。
- 如果一个字节的第一位为0,那么当前字符占用1个字节的空间。
- 如果一个字节的第一位为110,那么当前字符占用2个字节的空间。
- 如果一个字节的第一位为1110,那么当前字符占用3个字节的空间。
- 如果一个字节的第一位为11110,那么当前字符占用4个字节的空间。
也就是说根据首字节就可以判断出该字符占用的二进制串的长度。自然就可以得出一个无符号的数字,然后用这个数字在utf-8的编码表中找到对应的字符就可以了。到现在你已经完全掌握了utf-8编码。
下面锁一个小例子,把下面的utf-8编码的二进制串解码:
111001101000100010010001111001111000100010110001111001001011110110100000
当然我们可以按照8位为单元把二进制串转成字节数组,然后按照utf-8编码传递给String对象就可以得到想要的答案,这里为了理解utf-8编码,模拟解码过程。代码如下:
package utf8;
public class TestUtf8 {
public static void main(String[] args) throws Exception {
String binaryString = "111001101000100010010001111001111000100010110001111001001011110110100000";
while(!binaryString.equals("")){
binaryString = decode(binaryString);
}
}
/**
* 解码
* @param binaryString
* @return
* @throws Exception
*/
private static String decode(String binaryString)throws Exception{
//找到当前二进制串
String curBinaryString = findCurBinary(binaryString);
//把当前二进制串装为字符,也就是找数字对应的字符
binaryToChar(curBinaryString);
//范围剩余二进制串
return findLeftBinary(binaryString);
}
/**
* 找到当前二进制串
* @param binaryString
* @return
*/
private static String findCurBinary(String binaryString){
if(binaryString.startsWith("0")){//1个字节
return binaryString.substring(0,8);
}else if(binaryString.startsWith("110")){//2两个字节
return binaryString.substring(0,16);
}else if(binaryString.startsWith("1110")){//3个字节
return binaryString.substring(0,24);
}else if(binaryString.startsWith("11110")){//4个字节
return binaryString.substring(0,32);
}
return "";
}
/**
* 范围剩余二进制串
* @param binaryString
* @return
*/
private static String findLeftBinary(String binaryString){
if(binaryString.startsWith("0")){//1个字节
return binaryString.substring(8);
}else if(binaryString.startsWith("110")){//2两个字节
return binaryString.substring(16);
}else if(binaryString.startsWith("1110")){//3个字节
return binaryString.substring(24);
}else if(binaryString.startsWith("11110")){//4个字节
return binaryString.substring(32);
}
return "";
}
/**
* 模拟查找数字对应字符
* @return
*/
private static void binaryToChar(String binaryString)throws Exception{
byte [] bytes = new byte[binaryString.length()/8];
for(int i=0;i<bytes.length;i++){
String substring = binaryString.substring(i * 8, i * 8 + 8);
bytes[i] = Integer.valueOf(substring,2).byteValue();
}
String result = new String(bytes, "utf-8");
System.out.print(result);
}
}
当然你可以使用下面的方法:
package utf8;
public class TestUtf8 {
public static void main(String[] args) throws Exception {
String binaryString = "111001101000100010010001111001111000100010110001111001001011110110100000";
binaryToChar(binaryString);
}
/**
* 模拟查找数字对应字符
* @return
*/
private static void binaryToChar(String binaryString)throws Exception{
byte [] bytes = new byte[binaryString.length()/8];
for(int i=0;i<bytes.length;i++){
String substring = binaryString.substring(i * 8, i * 8 + 8);
bytes[i] = Integer.valueOf(substring,2).byteValue();
}
String result = new String(bytes, "utf-8");
System.out.print(result);
}
}
想知道结果运行程序即可。
当然知道了utf-8编码的原理,你还可以解决一些实际问题,比如你的mysql数据库编码是utf-8的,那么mysql默认的utf-8编码最多支持3个字节。这就会导致存储一些占用4个字节的内容会导致错误。这个时候你可以利用你掌握的原理,把首字节开头为11110的二进制串过滤掉,然后再进行存储。也可以利用二进制串表示数字大于FFFF的就过滤掉。当然最简单的方法就是把数据库编码换成utf8mb4,也就是支持4个字节的utf-8编码。