文章目录
字符编码与解码(附:Java字符流与字节流源码剖析)
1. 从二进制码到字符
- 我们先声明两点,在计算机中:
- 任何一个字节都是由二进制码组成的
- 任何一个字符都是由二进制码组成的,一个字符根据解码的方式不同,通常可以由字节组成(当然,字节同样也是二进制)
- 不同的编码/解码规范规定了一个字符该由什么样的二进制码表示,例如:
- ASCII中,二进制码’0110 1000’代表了字符’h’
- GBK中,二进制码’11111111111111111111111111001010 11111111111111111111111111000000’代表了字符’世’
- UTF-8中,二进制码’11111111111111111111111111100100 11111111111111111111111110111000 11111111111111111111111110010110’代表了字符’世’
- 通过使用编码/解码规范(ASCII、GBK、UTF-8等),我们可以实现字符与二进制码的转换
- 编码:将字符转为二进制码(计算机能够识别,用于计算机的存储、网络传输)
- 解码:将二进制码转为字符(便于人类识别)
- 显然,采用不同的编码、解码规范很可能会导致数据的编码或解码错误,例如:
- 将一个字符’世’,先用GBK规范编码,再用UTF-8规范解码(对照前面字符’世’的不同二进制码可知)
2. 举例:UTF-8中的编码/解码
- 首先,我们要知道UTF-8规范是按8bit(1byte)一次的方式转换二进制码的,并且它是变长多字节编码:
- 一个英文字母用1byte表示
- 一个汉字用3byte表示
2.1 UTF-8 编码
- 那么,我们用Java写一份代码来看一下吧
String content = "hello 世界"; byte[] bytes = content.getBytes("UTF-8"); // 使用UTF-8编码,转为字节数组(即二进制码) for (int i = 0; i < bytes.length; i++) { byte current = bytes[i]; System.out.println( i + " -> " + "十进制: " + current + ", 十六进制: " + Integer.toHexString(current) + ", 二进制: " + Integer.toBinaryString(current) ); }
- 输出结果如下
0 -> 十进制: 104, 十六进制: 68, 二进制: 1101000 1 -> 十进制: 101, 十六进制: 65, 二进制: 1100101 2 -> 十进制: 108, 十六进制: 6c, 二进制: 1101100 3 -> 十进制: 108, 十六进制: 6c, 二进制: 1101100 4 -> 十进制: 111, 十六进制: 6f, 二进制: 1101111 5 -> 十进制: 32, 十六进制: 20, 二进制: 100000 6 -> 十进制: -28, 十六进制: ffffffe4, 二进制: 11111111111111111111111111100100 7 -> 十进制: -72, 十六进制: ffffffb8, 二进制: 11111111111111111111111110111000 8 -> 十进制: -106, 十六进制: ffffff96, 二进制: 11111111111111111111111110010110 9 -> 十进制: -25, 十六进制: ffffffe7, 二进制: 11111111111111111111111111100111 10 -> 十进制: -107, 十六进制: ffffff95, 二进制: 11111111111111111111111110010101 11 -> 十进制: -116, 十六进制: ffffff8c, 二进制: 11111111111111111111111110001100
- 解释一下
- 前面6个byte是英文字符与一般符号,同ASCII的方式编码,每个字符编码为1byte,例如
- ‘h’ -> ‘1101000’
- ‘e’ -> ‘1100101’
- 后面6个byte是中文汉字,每个字符编码为3byte,例如
- ‘世’ -> ‘11111111111111111111111111100100’和’11111111111111111111111110111000’和’11111111111111111111111110010110’
- 前面6个byte是英文字符与一般符号,同ASCII的方式编码,每个字符编码为1byte,例如
2.2 UTF-8 解码
- 我们再用二进制码构造两个汉字试试(UTF-8方式解码)
byte shi1 = 0b11111111111111111111111111100100; byte shi2 = 0b11111111111111111111111110111000; byte shi3 = 0b11111111111111111111111110010110; byte[] shi = {shi1, shi2, shi3}; System.out.println(new String(shi, "UTF-8")); byte jie1 = 0b11111111111111111111111111100111; byte jie2 = 0b11111111111111111111111110010101; byte jie3 = 0b11111111111111111111111110001100; byte[] jie = {jie1, jie2, jie3}; System.out.println(new String(jie, "UTF-8"));
- 输出如下
世 界
2.3 错误的编码与解码
- 如果我们先用GBK规范编码,再用UTF-8规范解码,会发生什么事情?
String content = "hello 世界"; byte[] gbkBytes = content.getBytes("GBK"); String utf8Str = new String(gbkBytes, "UTF-8"); System.out.println(utf8Str); System.out.println(content);
- 输出如下
hello ���� hello 世界
- 显然,因为GBK和UTF-8对于英文字符的解析方式相同,所以’hello '部分没有出错,但是因为对于汉字的编码/解码规定不同,导致了乱码
- 另外,如果一个文本文件是GBK编码的,使用ISO-8859-1解码,再使用ISO-8859-1编码,最后存储
- 使用ISO-8859-1解码,如果打印字符,显然是乱码的
- 存储的结果则是没有问题的,因为ISO-8859-1是单字节编码,并且使用了单字节内的所有空间。因此将任何字节流按ISO-8859-1解码,再编码,都不会有丢失的问题。(MySQL默认的编码Latin1就是如此)
- GBK与ISO-8859-1的示例代码如下
import java.io.FileInputStream; import java.io.FileOutputStream; public class Demo02 { public static void main(String[] args) throws Exception { String content = "hello 世界"; String path = "./test.txt"; // 对字符串进行GBK编码,并存储 byte[] gbkBytes = content.getBytes("GBK"); saveByteArray(path, gbkBytes, 0, gbkBytes.length); // 读取该文件字节码 byte[] bytes = new byte[1024]; int len = readByteArray(path, bytes); System.out.println("len = " + len); // 使用GBK解码,并打印 String gbkStr = new String(bytes, 0, len, "GBK"); System.out.println("gbkStr = " + gbkStr); // 使用iso-8859-1解码,并打印 String isoStr = new String(bytes, 0, len, "iso-8859-1"); System.out.println("isoStr = " + isoStr); // 使用iso-8859-1对该字符串进行编码,然后存储 byte[] isoBytes = isoStr.getBytes("iso-8859-1"); saveByteArray(path, isoBytes, 0, isoBytes.length); } public static void saveByteArray(String path, byte[] bytes, int off, int len) throws Exception { FileOutputStream fos = new FileOutputStream(path); fos.write(bytes); fos.close(); } public static int readByteArray(String path, byte[] bytes) throws Exception { FileInputStream fis = new FileInputStream(path); int len = fis.read(bytes); fis.close(); return len; } }
3. 字符编码的记录
-
ASCII
- 美国信息交换标准代码
- 7 bit 表示一个字符
- 共128个字符
-
ISO-8859-1
- 对于ASCII的扩展
- 8 bit 表示一个字符,会使用整个byte
- 共256个字符
-
GB2312
- 国标,汉字的编码集
- 2 byte 表示一个字符
- 共6763个汉字
-
GBK
- 对于GB2312的扩展,能表示更多的字符
- 2 byte 表示一个字符
- 共21003个汉字
-
GB18030
- 对于GBK的扩展,最完整的汉字编码集
- 变长多字节编码,1个、2个或4个byte表示一个字符
- 共70000余个汉字
-
BIG5
- 由台湾制定,主要用于繁体汉字编码
- 2 byte 表示一个字符
- 共13060个汉字
-
Unicode
- 由国际标准化组织制定,整合全世界的字符
- 2 byte 表示一个字符
- 表示全世界所有的字符
- 如果只使用英文字符,较浪费空间
-
UTF(Unicode Translation Format)
- 通用转换格式,是Unicode的实现,解决了Unicode空间浪费的问题
- UTF-8, UTF-16, UTF-16LE(little endian), UTF-16BE(big endian), UTF-32
-
UTF-8
- 变长多字节编码,1~4字节表示一个字符
- 1 byte 表示一个US-ASCIl字符
- 2 byte 表示一个拉丁文字符(拉丁文、希腊文、西里尔字母、亚美尼亚语、希伯来文、阿拉伯文、叙利亚文等)
- 3 byte 表示一个汉字(中日韩文字、东南亚文字、中东文字等)
- 4 byte 表示其他极少使用的语言
- 变长多字节编码,1~4字节表示一个字符
-
UTF-8-BOM(Byte Order Mark)
- Unicode规定使用BOM来标识字节顺序,UTF-8-BOM的文件会以EF BB BF开头
- UTF-16和UTF-32需要决定是按2Byte读还是按4byte读,需要BOM来决定顺序
- UTF-8是按1byte读的,没有字节序问题,是不需要BOM来标识字节序的
- 建议:使用UTF-8时,最好使用不带BOM的UTF-8
4. Java中字符流与字节流的关系(源码剖析)
- 我们从字符流入手,先看java.io.FileReader,它继承于java.io.InputStreamReader,其构造器如下
public FileReader(String fileName) throws FileNotFoundException { super(new FileInputStream(fileName)); } public FileReader(File file) throws FileNotFoundException { super(new FileInputStream(file)); } public FileReader(FileDescriptor fd) { super(new FileInputStream(fd)); }
- 其构造器都是会new FileInputStream(),而FileInputStream继承于InputStream
- FileInputStream会通过native方法open0获取到输入字节流,其代码如下
public FileInputStream(File file) throws FileNotFoundException { String name = (file != null ? file.getPath() : null); SecurityManager security = System.getSecurityManager(); if (security != null) { security.checkRead(name); } if (name == null) { throw new NullPointerException(); } if (file.isInvalid()) { throw new FileNotFoundException("Invalid file path"); } fd = new FileDescriptor(); fd.attach(this); path = name; open(name); } private void open(String name) throws FileNotFoundException { open0(name); } private native void open0(String name) throws FileNotFoundException;
- 接着FileReader的构造器,调用了其父类InputStreamReader的构造器(super方法),将FileInputStream传了进去
- InputStreamReader实际是InputStream的包装类,对其进行功能增强(提供字符解码能力)。在构造器中,利用StreamDecoder对InputStream进行解码
- InputStreamReader构造器代码如下
public InputStreamReader(InputStream in) { super(in); try { sd = StreamDecoder.forInputStreamReader(in, this, (String)null); // ## check lock object } catch (UnsupportedEncodingException e) { // The default encoding should always be available throw new Error(e); } }
- StreamDecoder中的解码方法会先看是否指定了Charset(例如UTF-8),如果没指定会使用默认的Charset,最后如果系统支持该Charset,那么会返回StreamDecoder,代码如下
public static StreamDecoder forInputStreamReader(InputStream var0, Object var1, String var2) throws UnsupportedEncodingException { String var3 = var2; if (var2 == null) { var3 = Charset.defaultCharset().name(); } try { if (Charset.isSupported(var3)) { return new StreamDecoder(var0, var1, Charset.forName(var3)); } } catch (IllegalCharsetNameException var5) { } throw new UnsupportedEncodingException(var3); }
- 而在InputStreamReader中,会将StreamDecoder赋值给全局变量sd,后续的读取相关方法,皆是调用sd的read方法,代码如下
public String getEncoding() { return sd.getEncoding(); } public int read() throws IOException { return sd.read(); } public int read(char cbuf[], int offset, int length) throws IOException { return sd.read(cbuf, offset, length); } public boolean ready() throws IOException { return sd.ready(); } public void close() throws IOException { sd.close(); }
- 显然,我们可以知道Java中字符流与字节流的关系,其实就是字符流是对字节流功能的增强(编码/解码),本质上字符流用到的还是字节流