准备知识
在计算机内存中,存储的是二进制数据,在网络传输中,也是二进制数据,以字节(byte)为单位。1个字节(byte)=8位,每个二进制位(bit)都有0和1两种状态,因此从00000000~11111111可以用来表示256个不同的符号。字符编码即用二进制数来表示各种各样的字符。
1.ASCII字符编码
ASCII一共规定了128个字符的编码,如:A是65(二进制为01000001)。这128个字符只占用了一个字节的后7位,最前面一位统一规定为0。ASCII编码不能解决的问题:英语使用128个符号就够了,而对于其它语言,128个符号是不够的,比如汉字就多达10万左右。一个字节只能表示256个符号,因此必须使用多个字节来表示一个符号。
在多种编码方式中,同一个二进制数可以被理解成不同的符号。因此,每打开一个文本文件,都必须知道它的编码方式,若使用错误的方式,就会出现乱码。 因此,我们需要一种编码,将世界上所有的符号都纳入其中,每个符号都一一对应一个编码,那么乱码问题就会解决。因此出现了Unicode,即一种所有符号的编码,可以容纳100多万个字符,每个符号的编码都不相同。
2.Unicode编码
需要注意的是:Unicode仅仅是一个符号集,它规定了符号的二进制代码。而这个二进制代码在计算机中如何存储,则是UTF-8所解决的问题。汉字'沐'的Unicode编码为'6C90',转换为2进制为'110 1100 1001 0000'共15位,因此'沐'至少需要2个字节来表示。表示其它更大的符号时,可能需要3个字节甚至更多。而这将带来2个严重问题:
- 计算机如何确定多少个字节表示1个符号?
- 英文字符使用1个字节表示就足够了,如果统一使用Unicode规范,每个符号使用3个或4个字节表示,那个每个英文字母前面都必然有2到3个字节是0,这对于存储来说是极大的浪费,文本文件的大小因此会大出2-3倍,这是无法接受的!
UTF-8为我们很好地实现了Unicode编码,解决了以上2个存储上的问题。
3.UTF-8 Unicode的实现方式之一
一种使用广泛的Unicode实现方式,其它实现方式还包含:UTF-16(使用2/4个字节表示)和UTF-32(使用4个字节表示)。UTF-8最大的特点是其变长的编码方式,使用1-4个字节表示一个符号,根据不同的符号而变换字节长度。UTF-8的编码规则:
- 对于单字节的符号,字节第一位设为0,后面7位为这个符号的unicode码。对于英语字母,UTF-8编码和ASCII编码相同。
- 对于n(n>1)字节的符号:第1个字节的前n位都设为1,第n+1位设为0,后面字节的前2位一律设为10。剩下的二进制位,全部为这个符号的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 |
下面以汉字'沐'为例,演示如何实现UTF-8编码:
- '沐'的Unicode编码为:0000 6C90;
- '沐'对应表格中的范围为0000 0800 ~ 0000 FFFF,因此沐的编码需要3个字节,格式为'1110xxxx 10xxxxxx 10xxxxxx';
- 将'沐'的二进制(110110010010000)Unicode码依次填入X位置,高位补0:"11100110 10110010 10010000"转换为十六进制就是:0xe6b290,即'沐'的utf-8编码。
4.Java中的编码/解码问题
编码 | String.getBytes() | 按照当前文件的编码方式(Sytem.getProperty("file.encoding"))返回字符串的byte[] array |
String.getBytes(charset) | 按照指定的charset返回字符串的byte[] array | |
解码 | new String(byte[] byteArr, charset) | 将字节数组解码为相应的字符串。 |
示例1:验证标题3中“沐”的utf-8编码是否正确:
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
// mu在内存中以Unicode编码形式存在
char mu = '\u6C90';
System.out.println(mu);
// 使用"沐"的utf-8编码,验证解码后是否为'沐'
byte[] b = {(byte)0xe6, (byte)0xb2, (byte)0x90};
String str = new String(b, StandardCharsets.UTF_8);
System.out.println(Arrays.toString(b)+str);
// 扩展:获取字符串指定编码的字节数组
byte[] b1 = "沐".getBytes(StandardCharsets.UTF_8);
System.out.println(Arrays.toString(b1)+"沐");
// n & 0xFF将补码转换为原码
for (int n : b1){
System.out.print(Integer.toHexString(n & 0xFF));
System.out.print(",");
}
}
}
运行结果:
说明:java中byte的大小为1字节,范围是-128~+127;且java中数值的二进制是以补码形式存储的,因此byte[]中会出现负数。补码的出现,主要是将计算机中的减法运算转化成加法运算,降低计算机底层的复杂性。
示例2:编码,解码,乱码:
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
public class Main2 {
public static void main(String[] args) throws UnsupportedEncodingException {
// 获取指定编码格式的字符串
byte[] gbkByte = "沐".getBytes("GBK");
byte[] utf8Byte = "沐".getBytes(StandardCharsets.UTF_8);
byte[] iso88591Byte = "沐".getBytes(StandardCharsets.ISO_8859_1);
System.out.print("gbk byte length:"+gbkByte.length+"; ");
System.out.println(Arrays.toString(gbkByte));
System.out.print("utf-8 byte length:"+utf8Byte.length+"; ");
System.out.println(Arrays.toString(utf8Byte));
System.out.print("iso88591 byte length:"+iso88591Byte.length+"; ");
System.out.println(Arrays.toString(iso88591Byte));
// 解码还原
String gbkString = new String(gbkByte, "gbk");
String utf8String = new String(utf8Byte, StandardCharsets.UTF_8);
String iso88591String = new String(iso88591Byte, StandardCharsets.ISO_8859_1);
System.out.println("gbk:"+gbkString+";utf-8:"+utf8String+";iso88591:"+iso88591String);
}
}
运行结果:
运行结果中发现,最后一行输出,iso88591编码"沐"再解码"沐"出现了乱码。原因很简单,ISO8859-1编码是欧洲使用的一种标准,编码表根本就不包含汉字字符。
示例3:ISO88591解决中文编码乱码问题
http请求头的编码方式为iso88591,要解决包含中文的问题,通常可以将中文字符串按指定编码方式编码,再有ISO88591解码和编码,还原最初的byte[],接收端使用原始的编码方式解码。(即便中间的过程是乱码,也没有关系,因为ISO88591总是一个字符对应一个字节)。
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
public class Main {
public static void main(String[] args) throws UnsupportedEncodingException {
String s = "沐";
byte[] utf8 = s.getBytes(StandardCharsets.UTF_8);// 使用utf-8编码
System.out.println(Arrays.toString(utf8));
// 使用UTF-8编码,再使用ISO-8859-1解码和编码,接收端使用UTF-8解码即可
String s2 = new String(utf8, StandardCharsets.ISO_8859_1);// 使用ISO-8859-1解码
System.out.println(Arrays.toString(s2.getBytes(StandardCharsets.ISO_8859_1)));// 还原字节数组
System.out.println(new String(s2.getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8));
byte[] gbk = s.getBytes("gbk");// 使用gbk编码
System.out.println(Arrays.toString(gbk));
// 使用gbk编码,再使用ISO-8859-1解码和编码,接收端使用gbk解码即可
String s3 = new String(gbk, StandardCharsets.ISO_8859_1);
System.out.println(Arrays.toString(s3.getBytes(StandardCharsets.ISO_8859_1)));
System.out.println(new String(s3.getBytes(StandardCharsets.ISO_8859_1), "gbk"));
}
}
运行结果: