特别鸣谢 :最近达内的刘苍松老师给我发了一段自己实现UTF-8编码的代码,我查阅了相关资料,并获得老师同意,特此记录一下 |
---|
1. Unicode |
---|
- 这是一种
所有符号的编码
。将世界上所有的符号都纳入其中。每一个符号都给予一个独一无二的编码,让乱码问题消失了。- Unicode
只是一个符号集
,它只规定了符号的二进制代码,却没有规定
这个二进制代码应该如何存储
- 但是,如何才能区别 Unicode 和 ASCII ?计算机怎么知道三个字节表示一个符号,而不是分别表示三个符号呢?英文字母只用一个字节表示就够了,如果 Unicode 统一规定,每个符号用三个或四个字节表示,每个英文字母前都必然有二到三个字节是0,对于存储是极大的浪费,文本文件大小会大出二三倍
因此,出现了 Unicode 的多种存储方式
,也就是说有许多种不同的二进制格式,可以用来表示 Unicode- Unicode 在很长一段时间内无法推广,直到互联网的出现
而最大的问题, Unicode 的多种存储方式,让编码极度不统一,一个用第一种存储方式存储的文本,到其它人电脑上,很可能因为存储方式不同,出现乱码
2. UTF-8编码 |
---|
- 随着互联网的普及,强烈要求出现一种
统一的编码方式
(各国家共同遵守)。UTF-8 就是在互联网上使用最广的一种 Unicode 的实现方式,当然还有UTF-16和UTF-32- UTF-8 就表示 字符最小需要用一个字节来表示,最大4个字节,UTF-16最小需要用2个字节来表示,UTF-32需要4个字节来表示
- UTF-8 最大的一个特点,就是它是一种变长的编码方式。它可以使用1~4个字节表示一个符号,根据不同的符号而变化字节长度
3. UTF-8 的编码规则很简单 |
---|
- 对于单字节的符号,字节的第一位设为0,后面7位为这个符号的 Unicode 码。因此对于英语字母,UTF-8 编码和 ASCII 码是相同的
- 对于n字节(2、3、4)字节的符号,第一个字节的前n位都设为1,第n + 1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的 Unicode 码
- 假设编码2字节符号,第一个字节,是几个字节就几个1开头,然后跟一个0,然后加上Unicode码。剩下的字节一律10开头,然后跟上Unicode码
- 编码后就是这样110xxxxx 10xxxxxx,x表示字符Unicode码
- 有人可能不理解,如果一个字符表示需要两个字节,为什么还能110开头,不会占用字符位数么?
- 其实仔细看上图Unicode符号编码范围,两个字节的编码只有0x07FF,也就是0b10000000 00000000 ~ 0b00000111 11111111,0b00000111 11111111前面正好五个0,正好可以填充110xxxxx 10xxxxxx,110+10正好5位
java中编码:以下内容都是从老师源码看来的 |
---|
1. java如何存储字符 |
---|
- Java中字符存储,是一个符号的Unicode编码
- 可以显示为10进制或16进制形式
- Java的字符范围 0 ~ 65536 (0xFFFF)
public class Test{
public static void main(String[] args) {
/*
* Unicode 编码
* - Java 中的字符存储的是一个符号的Unicode编码
* - 可以显示为 10进制或16进制形式
* - Java的字符范围 0 ~ 65535(FFFF)
*/
char c1 = 'A'; // 41 65
char c2 = '中'; // 4e2d 20013
char c3 = '듏'; // U+B4CF 46287
char c4 = 'α'; // U+03B1 945
System.out.println((int)c1);//'A'十进制65,强转,字符转成Unicode码,英文和ASCII编码一样
System.out.println(Integer.toHexString(c1));//A十六进制41
System.out.println((int)c2);//'中'十进制20013
System.out.println(Integer.toHexString(c2));//'中'十六进制4e2d
System.out.println((int)c3);//'듏'十进制46287
System.out.println(Integer.toHexString(c3));//듏'十六进制b4cf
System.out.println((int)c4);//'α'十进制945
System.out.println(Integer.toHexString(c4));//'α'十六进制3b1
}
}
2. 如何将Unicode编码为UTF-8 |
---|
- 要编码字符c,若<= 0x7F,表示单字节字符,就是ASCII,直接转即可
- c <= 0x7FF,处理两个字节的UTF-8编码处理成110xxxxx 10xxxxxx,只需要取出c字符的Unicode编码然后加前缀即可
- 将两个字节分开处理,b1为110xxxxx,b2为10xxxxxx
- b2截取字符的后6位,c & 00111111 可以拿到c的后6位,然后 | 10000000 正好填充10在前面
- b1截取剩下的5位,(c >>> 6)先把后6为右移,方便我们运算剩下的5位,然后 & 00011111,拿到这5位,最后 | 11000000,正好填充110在前面
- 3个字节,4个字节情况同理
3. 如何将UTF-8反编码为Unicode |
---|
- 如果编码c,我们定义为b1,右移7位,如果结果为0(b1 >>> 7) == 0,表示是单字节,直接强转
- 如果(b1 >>> 5) == 0b110,表示两个字节,我们需要把它前一个字节也拿出来b2,这两个字节加起来才是一个字符(b1+b2),b2 & 0b111111,将10前缀去掉,b1&0b11111,将110前缀去掉,然后左位移6位,给b2腾地方,然后哥俩拼接b1|b2
- 其它的同理
代码 |
---|
import java.util.Arrays;
public class Test{
/*
* 手工编写 UTF-8 编码
Char. number range | UTF-8 octet sequence
(hexadecimal) | (binary)
--------------------+---------------------------------------------
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
*/
/**
* 将一个字符串编码为 UTF-8 字节数组
* @param str 被编码的字符串
* @return 经过UTF-8编码以后字节数组
*/
public static byte[] getBytes(String str){
//预估一下返回值的最大情况,一个字符最大4字节
byte[] bytes = new byte[str.length() * 4];
//index 代表 bytes 数组中数据的存储位置
int index = 0;
//遍历字符串中每个字符,根据字符的Unicode编码范围,进行UTF-8编码
//将编存储到 bytes,bytes中就是返回值UTF-8数据
// str = Java
for(int i=0; i<str.length(); i++){
char c = str.charAt(i);
//判断c范围,根据范围进行编码
if(c <= 0x7F){//0b0000-0000 ~ 0b0111-1111
// c 在 0 ~ 0x7F 范围内,是1字节编码,1字节编码添加到bytes
bytes[index++] = (byte)c;//一字节编码就是ASCII,直接转即可
}else if(c <= 0x7FF){//0b10000000 00000000 ~ 0b00000111 11111111
// c 在 0x80 ~ 0x7FF 范围,处理两个字节的UTF-8编码
// b1 b2 将这两个字符
// 110xxxxx 10xxxxxx 处理成UTF-8编码格式
// b2截取字符的后6位
// c & 00111111 可以拿到c的后6位,然后 | 10000000 正好填充10在前面
//00000011 10111011 c
//00000000 00111111 0x3f
//00000000 00111011 c & 0x3f
// 10000000
// 10111011 c & 0x3f | 10000000
int b2 = (c & 0x3f) | 0b10000000; //0b0011-1111
// b1截取剩下的5位
// (c >>> 6)先把后6为右移,方便我们运算剩下的5位,然后 & 00011111,拿到这5位,最后 | 11000000,正好填充110在前面
//00000011 10111011 c
//00000000 00001110 c >>> 6 无符号右位移6位
//00000000 00011111 0x1f
//00000000 00001110 (c >>> 6) & 0x1f
// 11000000
// 11001110 (c >>> 6) & 0x1f | 11000000
int b1 = ((c >>> 6) & 0x1f) | 0b11000000; //0b11111
bytes[index++] = (byte)b1;
bytes[index++] = (byte)b2;
}else if (c<0xffff){
//处理3字节编码
//1110xxxx 10xxxxxx 10xxxxxx
int b3 = (c & 0b111111) | 0b10000000;
int b2 = ((c >>> 6) & 0b111111) | 0b10000000;
int b1 = ((c >>> 12) & 0b1111) | 0b11100000;
bytes[index++] = (byte) b1;
bytes[index++] = (byte) b2;
bytes[index++] = (byte) b3;
}
}
return Arrays.copyOf(bytes, index);
}
/**
* 将UTF-8编码的字节数组解码为字符串(Unicode字符)
* @param bytes UTF-8 编码的字节
* @return 解码以后的字符串
*/
public static String decode(byte[] bytes){
char[] chs = new char[bytes.length];
int index = 0;
//遍历 字节 数组,检查每个字节:
// 如果字节以0开头 则是单字节编码 0xxxxxxx
// 如果是以 110 为开头 则是双字节编码 110xxxxx
// 如果是 1110 开头则是3字节编码
for (int i = 0; i < bytes.length ; ){
int b1 = bytes[i++] & 0xff;
if((b1 >>> 7) == 0){
//检查 01001010 是否为 单字节编码 0xxxxxxx
// b1 00000000 00000000 00000000 01001010
// b1>>>7 000000000000000 00000000 00000000 0
chs[index++] = (char) b1;
}else if((b1 >>> 5) == 0b110){
//检查是否为双字节编码 b1 11001110 b2 10111011
// b1 -> int
// b1 00000000 00000000 00000000 11001110
// b1>>>5 0000000000000 00000000 00000000 110
int b2 = bytes[i++] & 0xff;
// b1 00000000 00000000 00000000 11001110
// b2 00000000 00000000 00000000 10111011
// c 00000000 00000000 0000001110 111011
int c = ((b1 & 0b11111)<<6) | (b2 & 0b111111);
chs[index++] = (char) c;
}else if ((b1 >>> 4) == 0b1110){
// 检查是否为3字节编码: 11101000 10100001 10101000
int b2 = bytes[i++] & 0xff;
int b3 = bytes[i++] & 0xff;
int c = ((b1 & 0b1111)<<12) | ((b2 & 0b111111)<<6) | (b3 & 0b111111);
//System.out.println("b1:" + Integer.toBinaryString(b1));
//System.out.println("b2:" + Integer.toBinaryString(b2));
//System.out.println("b3:" + Integer.toBinaryString(b3));
//System.out.println("c:" + Integer.toBinaryString(c));
chs[index++] = (char) c;
}
}
return new String(chs, 0, index);
}
public static void main(String[] args) {
String str = "Javaλ表达式";
System.out.println("Unicode:");
for(int i=0; i<str.length(); i++){
char c = str.charAt(i);
System.out.print(c);
System.out.print(":");
System.out.println(Integer.toBinaryString(c));
}
//调用手写UTF-8编码方法
byte[] bytes = getBytes(str);
for(byte b:bytes){
System.out.println(Integer.toBinaryString(b & 0xff));
}
//检查手写的UTF-8解码运算
String s = decode(bytes);
System.out.println(s);
}
}
java JDK提供的相关API |
---|
import java.nio.charset.StandardCharsets;
public class Utf8Demo {
public static void main(String[] args) throws Exception {
/*
* 测试UTF-8编解码API
*/
String str = "Javaλ表达式";
//将字符串中的文字进行UTF-8编码
// str.getBytes(StandardCharsets.UTF_8) 也可以写成 str.getBytes("UTF-8")
// 经过getBytes方法的转换得到了 UTF-8 编码的字节数组
byte[] bytes = str.getBytes(StandardCharsets.UTF_8);
// bytes 就可以利用网络进行传输
// 将字节数组中的 UTF-8 编码的字符进行解码
// new String(bytes, "UTF-8");
String s = new String(bytes, StandardCharsets.UTF_8);
System.out.println(s);
/*
* 输出字符串中每个字符的Unicode
* String str = "Javaλ表达式";
*/
System.out.println("Unicode:");
for(int i=0; i<str.length(); i++){
//i = 0 1 2 3 4 ...
char c = str.charAt(i);
System.out.print(c);
System.out.print(":");
System.out.println(Integer.toBinaryString(c));
}
System.out.println("UTF-8");
for(byte b:bytes){
System.out.println(Integer.toBinaryString(b & 0xff ));
}
}
}