参考链接:
Unicode 和 UTF-8 有什么区别?
Python2和Python3中字符串编码问题详解


Unicode 和 UTF-8 的区别

Unicode 是字符集,也叫编码表
UTF-8 是编码规则
字符集:为每一个「字符」分配一个唯一的 ID(学名为码位/码点/ Code Point)
编码规则:将「码位」转换为字节序列的规则(编码/解码)

字符集和编码表

一套字符集必然至少有一套字符编码。

  • ASCII字符集

    • ASCII(American Standard Code for Information Interchange,美国信息交换标准代码)是基于拉丁字母的一套电脑编码系统,用于显示现代英语,主要包括控制字符(回车键、退格、换行键等)和可显示字符(英文大小写字符、阿拉伯数字和西文符号)。

    • 基本的ASCII字符集,使用7位(bits)表示一个字符,共128字符。ASCII的扩展字符集使用8位(bits)表示一个字符,共256字符,方便支持欧洲常用字符。

  • ISO-8859-1字符集

    • 拉丁码表,别名Latin-1,用于显示欧洲使用的语言,包括荷兰、丹麦、德语、意大利语、西班牙语等。

    • ISO-5559-1使用单字节编码,兼容ASCII编码。

  • GBxxx字符集

    • GB就是国标的意思,是为了显示中文而设计的一套字符集。

    • GB2312:简体中文码表。一个小于127的字符的意义与原来相同。但两个大于127的字符连在一起时,就表示一个汉字,这样大约可以组合了包含7000多个简体汉字,此外数学符号、罗马希腊的字母、日文的假名们都编进去了,连在ASCII里本来就有的数字、标点、字母都统统重新编了两个字节长的编码,这就是常说的"全角"字符,而原来在127号以下的那些就叫"半角"字符了。

    • GBK:最常用的中文码表。是在GB2312标准基础上的扩展规范,使用了双字节编码方案,共收录了21003个汉字,完全兼容GB2312标准,同时支持繁体汉字以及日韩汉字等。

    • GB18030:最新的中文码表。收录汉字70244个,采用多字节编码,每个字可以由1个、2个或4个字节组成。支持中国国内少数民族的文字,同时支持繁体汉字以及日韩汉字等。

  • Unicode字符集

    • Unicode编码系统为表达任意语言的任意字符而设计,是业界的一种标准,也称为统一码、标准万国码。

    • 它最多使用4个字节的数字来表达每个字母、符号,或者文字。有三种编码方案,UTF-8、UTF-16和UTF-32。最为常用的UTF-8编码。

    • UTF-8编码,可以用来表示Unicode标准中任何字符,它是电子邮件、网页及其他存储或传送文字的应用中,优先采用的编码。互联网工程工作小组(IETF)要求所有互联网协议都必须支持UTF-8编码。所以,我们开发Web应用,也要使用UTF-8编码。它使用一至四个字节为每个字符编码,编码规则:

      1. 128个US-ASCII字符,只需一个字节编码。
      2. 拉丁文等字符,需要二个字节编码。
      3. 大部分常用字(含中文),使用三个字节编码。
      4. 其他极少使用的Unicode辅助字符,使用四字节编码。

编码和解码

字符需要有效传输,所以需要将字符编码为二进制的字节序列,解码就是从二进制序列,按照指定的规则,恢复出原始的内容(Unicode)。

编码:字符(能看懂的)→编码 encode→指定字节序列(看不懂的)
解码:指定字节序列(看不懂的) →解码 decode→字符(能看懂的)

UTF-8 编码例子

看个例子:It's 知乎日报

你看到的 Unicode 字符集是这样的编码表:

I  0049
t  0074
'  0027
s  0073
   0020
知 77e5
乎 4e4e
日 65e5
报 62a5

每一个字符对应一个十六进制数字。

计算机只懂二进制,因此,严格按照 Unicode 的方式,应该这样存储:

I  00000000 01001001
t  00000000 01110100
'  00000000 00100111
s  00000000 01110011
   00000000 00100000
知 01110111 11100101
乎 01001110 01001110
日 01100101 11100101
报 01100010 10100101

这个字符串总共占用了 18 个字节,但是对比中英文的二进制码,可以发现,英文前 9 位都是 0!浪费啊,浪费硬盘,浪费流量。

UTF-8 为了节省空间,这样做:

  • 单字节的字符,字节的第一位设为 0,对于英语文本,UTF-8 码只占用一个字节,和 ASCII 码完全相同。
  • n 个字节的字符(n>1),第一个字节的前 n 位设为 1,第 n+1 位设为 0,后面字节的前两位都设为 10,这 n 个字节的其余空位填充该字符 Unicode 码,高位用 0 补足。

这样就形成了如下的 UTF-8 标记位:

0xxxxxxx
110xxxxx 10xxxxxx
1110xxxx 10xxxxxx 10xxxxxx
11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
... ...

于是,”It's 知乎日报“就变成了:

I  01001001
t  01110100
'  00100111
s  01110011
   00100000
知 11100111 10011111 10100101
乎 11100100 10111001 10001110
日 11100110 10010111 10100101
报 11100110 10001010 10100101

和上边的方案对比一下,英文短了,每个中文字符却多用了一个字节。但是整个字符串只用了 17 个字节,比上边的 18 个短了,所以 UTF-8 编码所占用的空间少了。

Java 中关于编码的理解

在 Java 的内存中,字符(String,char)都以 Unicode 编码存储,使用 Java 内码,UTF-16。虽然 .java.class 文件是 UTF-8 格式,加载进内存的时候,会将字符解码为 Unicode。

当进行编码的时候,会改变二进制在内存中的样子,只能通过对应的解码规则进行解码,在内存中变成 Unicode 字符,我们才能正常看到。

String.getBytes()是一个用于将String的内码转换为指定的外码的方法。
无参数使用平台的默认编码作为外码,有参数版使用参数指定的编码作为外码。
将String的内容用外码编码好,结果放在一个新byte[]返回。

内存存储的是UTF-16编码的字节,那么String.getBytes("UTF-8")应该是从UTF-16到UTF-8的转换过程。

验证编码:

package package1;

import java.io.IOException;

public class Main {
    public static void main(String[] args) throws IOException {
        String name1 = "小aa";
        String name2 = "小aa";
        byte[] bytes1 = name1.getBytes("utf8");//5
        System.out.println(bytes1.length);
        byte[] bytes2 = name1.getBytes("gbk");//4
        System.out.println(bytes2.length);
    }
}

编码和解码时规则不一样时:

package package1;

import java.io.IOException;

public class Main {
    public static void main(String[] args) throws IOException {
        String name = "小aa";
        byte[] bytes = name.getBytes("utf-8");
        String s = new String(bytes, "utf-8");
        System.out.println(s);//小aa

        byte[] bytes2 = name.getBytes("utf-8");
        String s2 = new String(bytes, "gbk");
        System.out.println(s2);//灏廰a
    }
}