字符编码与解码(附:Java字符流与字节流源码剖析)

字符编码与解码(附: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’

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 表示其他极少使用的语言
  • 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中字符流与字节流的关系,其实就是字符流是对字节流功能的增强(编码/解码),本质上字符流用到的还是字节流
发布了128 篇原创文章 · 获赞 45 · 访问量 15万+

猜你喜欢

转载自blog.csdn.net/alionsss/article/details/103789906