UTF-8, UTF-16, UTF-32

参考文章

  1. http://unicode.org/faq/utf_bom.html
  2. 深入分析 Java 中的中文编码问题
  3. (wikipedia) Plane (Unicode)
  4. https://codepoints.net/
  5. (知乎) Unicode字符集中有哪些神奇的字符?
  6. Java Language Specification 的 3.1 小节
  7. (阮一峰) 字符编码笔记:ASCII,Unicode 和 UTF-8
  8. (MySQL 官方文档) 10.9.3 The utf8 Character Set (Alias for utf8mb3)
  9. Unicode 中有多少个 code point
  10. Families
  11. Unicode surrogate programming with the Java language
  12. () Unicode和UTF-8、UTF-16、UTF-32

问题

  1. 什么是 Unicode?
  2. Unicode 中的 code point 与 Java 中的 char 有何关系?
  3. utf-8,utf-16,utf-32 是什么?
  4. 在 Java 中如何遍历 String 里的 code point?
  5. MySQL 中的 utf8 和 utf8mb4 的区别是什么?

重要名词

  1. planeBasic Multilingual Plane
  2. code pointcode unit
  3. high surrogatelow surrogate

问题1: 什么是 Unicode?

Unicode provides a unique number for every character,
no matter what the platform,
no matter what the program,
no matter what the language.

引自 https://www.unicode.org/standard/WhatIsUnicode.html

Unicode 中定义了一些自然数(包括0)和 code point 之间的映射关系

Unicode 中的平面(plane)

在 Unicode 标准中, 每个 plane65536 个 code point 组成. 总共有 17plane, 编号从 016, Plane 16 里最后一个code pointU+10FFFF. Plane 0 被称为 Basic Multilingual Plane (BMP), 其中包含了最常用的 code point. Plane 1Plane 16 被称为 "supplementary planes".[1]

Unicode Planes

Unicode Planes

其中4个 plane 的分配情况(白底色表示未分配, 其他底色表示已分配)

  1. BMP(Basic Multilingual Plane, 即 Plane 0): U+0000..U+FFFF

    A map of the Basic Multilingual Plane. Each numbered box represents 256 code points.

  2. SMP(Supplementary Multilingual Plane, 即 Plane 1): U+10000..U+1FFFF

    A map of the Supplementary Multilingual Plane. Each numbered box represents 256 code points.

  3. SIP(Supplementary Ideographic Plane 即 Plane 2): U+20000..U+2FFFF

    A map of the Supplementary Ideographic Plane. Each numbered box represents 256 code points.

  4. SSP(Supplementary Special-purpose Plane, 即 Plane 14):U+E0000..U+EFFFF

    A map of the Supplementary Special-purpose Plane. Each numbered box represents 256 code points.

问题2: Unicode 中的 code point 与 Java 中的 char 有何关系?

The Unicode standard was originally designed as a fixed-width 16-bit character encoding. It has since been changed to allow for characters whose representation requires more than 16 bits. The range of legal code points is now U+0000 to U+10FFFF, using the hexadecimal U+n notation. Characters whose code points are greater than U+FFFF are called supplementary characters. To represent the complete range of characters using only 16-bit units, the Unicode standard defines an encoding called UTF-16. In this encoding, supplementary characters are represented as pairs of 16-bit code units, the first from the high-surrogates range, (U+D800 to U+DBFF), the second from the low-surrogates range (U+DC00 to U+DFFF). For characters in the range U+0000 to U+FFFF, the values of code points and UTF-16 code units are the same.

java 中的1个 char 相当于1个 code unit, 1个 code point 对应 1或2个 code unit, 具体如下

  1. code pointU+0000..U+D7FFU+E000..U+FFFF 范围内时, 用 1 个 code unit(或 1个Java中的 char)来表示这个 code point
  2. code pointU+10000..U+10FFFF 范围内时, 用 2 个 code unit(或 2个Java 中的 char)来表示这个 code point


如何用2个char来表示非BMP的code point:

  1. U+10000..U+10FFFF 范围内一共有 2^20 个 code point(0x10FFFF-0x10000+1=0x10000, 即2^20), 所以用20个bit可以区分这些 code point
  2. 高代理(high surrogate)有1024种可能取值, 低代理(low surrogate)也有1024种可能取值, 所以高代理和低代理组成的对(high, low)会有 1024 * 1024 种可能取值(而 1024 * 1024 = 2 ^ 20)
    高代理和低代理的位置
  3. 绿色边框的高代理区域共有1024个code point
  4. 蓝色边框的低代理区域共有1024个code point

code pointU+10000..U+10FFFF 范围内时, 对应的 2个code unit 的计算方法(伪代码)

delta = cp  - 0x10000 // 计算给定的 code point 和 0x10000 的差值

temp_high = (delta >> 10) // 取高10位
temp_low = (delta & 0x3FF) // 取低10位

high = temp_high + 0xD800 // 加上高代理的偏移量
low = temp_low + 0xDC00 // 加上低代理的偏移量

jdk 中高代理和低代理的计算

我们可以参考 Character.highSurrogate(int)Character.lowSurrogate(int) 的源码

高代理(high surrogate)的计算

Character.highSurrogate(int)

解释如下

public static char highSurrogate(int codePoint) {
    // MIN_HIGH_SURROGATE = '\uD800'
    // MIN_SUPPLEMENTARY_CODE_POINT = 0x10000
    // 计算步骤: 1. 计算差值; 2. 取差值高10个bit; 3. 加上高代理区域的偏移量
    // 计算步骤合在一起: ((codePoint - 0x10000) >>> 10) + 0xD800
    return (char) ((codePoint >>> 10)
            + (MIN_HIGH_SURROGATE - (MIN_SUPPLEMENTARY_CODE_POINT >>> 10)));
}

低代理(low surrogate)的计算

Character.lowSurrogate(int)

解释如下

public static char lowSurrogate(int codePoint) {
    // MIN_LOW_SURROGATE  = '\uDC00'
    // 由于 (codePoint - 0x10000) 的低10个bit和 codePoint 的低10个bit是一样的
    // 所以可以直接计算 codePoint 的低10个bit
    // 那么我们在取出 codePoint 的低10个bit后, 加上低代理区域的偏移量 0xDC00
    return (char) ((codePoint & 0x3ff) + MIN_LOW_SURROGATE);
}

如何向 StringBuilder append 一个 code point?

appendCodePoint

问题3: utf-8,utf-16,utf-32 是什么?

Q: What is a UTF?

A: A Unicode transformation format (UTF) is an algorithmic mapping from every Unicode code point (except surrogate code points) to a unique byte sequence. The ISO/IEC 10646 standard uses the term “UCS transformation format” for UTF; the two terms are merely synonyms for the same concept.

引自 https://www.unicode.org/faq/utf_bom.html
UTF 可以将 Unicode 中每个的 code point (除了U+D800..U+DFFF 范围内的 code point)映射为不同的字节序列

What are some of the differences between the UTFs?

图片来源
(wikipedia) BOM

  1. UTF-32BE
    code point 对应的整数用大端法的4个字节表示即可
    例如用 UTF-32BE 对 U+1F602 进行编码, 得到的字节序列为
0x00
0x01
0xF6
0x02



用 Java 来实现 UTF-32BE 编码并验证

package com.naive.wow;

import java.io.UnsupportedEncodingException;
import java.util.Arrays;
import java.util.Random;

public class NaiveUTF32 {
    public static void main(String[] args) throws UnsupportedEncodingException {
        Random random = new Random();

        for (int i = 0; i < 100; i++) {
            // 产生一个随机的 code point
            int cp = (int) (random.nextDouble() * 0x10FFFF);

            // U+D800..U+DFFF 上的 code point 不用验证
            if (cp >= 0xD800 && cp <= 0xDFFF) {
                continue;
            }

            String s = new String(new int[]{cp}, 0, 1);
            // bytes 中保存了正确的编码结果
            byte[] bytes = s.getBytes("UTF-32BE");

            // 将 cp 看成一个大端法表示的4字节数(不过 Java 中的 int 本来就是4字节的), 就可以得到其对应的 UTF-32BE 编码
            // calculated 中保存了我们自己计算的编码结果
            byte[] calculated = new byte[]{
                    0, // 第一个字节中的每一位都是 0
                    (byte) ((cp >> 16) & 0xFF), // 计算第二个字节
                    (byte) ((cp >> 8) & 0xFF), // 计算第三个字节
                    (byte) (cp & 0xFF), // 计算第四个字节
            };

            // 如果两者不一致, 会抛出异常(注意运行时要开启 -ea 选项才能启用断言功能)
            assert Arrays.equals(calculated, bytes);
        }
    }
}
  1. UTF-16BE

举个例子
求 UTF-16BE 对 U+1F602 进行编码的结果
下面的计算过程是在 Python3 中生成的

>>> cp = 0x1f602
>>> delta = cp - 0x10000
>>> high = (delta >> 10) + 0xD800
>>> low = (delta & 0x3FF) + 0xDC00
>>> print(hex(high))
0xd83d
>>> print(hex(low))
0xde02
>>> 

所以对 U+1F602 的编码结果为

0xD8
0x3D
0xDE
0x02
package com.naive.wow;

import java.io.UnsupportedEncodingException;
import java.util.Arrays;
import java.util.Random;

public class NaiveUTF16 {
    public static void main(String[] ars) throws UnsupportedEncodingException {
        Random random = new Random();

        for (int i = 0; i < 100; i++) {
            // 产生一个随机的 code point
            int cp = (int) (random.nextDouble() * 0x10FFFF);

            // U+D800..U+DFFF 上的 code point 不用验证
            if (cp >= 0xD800 && cp <= 0xDFFF) {
                continue;
            }

            String s = new String(new int[]{cp}, 0, 1);
            // bytes 中保存了正确的编码结果
            byte[] bytes = s.getBytes("UTF-16BE");

            if (cp < 0x10000) {
                // 如果 cp 在 BMP 上, 则对应1个 code unit(也就是2个byte)
                byte[] calculated = new byte[]{
                        (byte) (cp >> 8), (byte) (cp & 0xFF)
                };

                // 如果两者不一致, 会抛出异常(注意运行时要开启 -ea 选项才能启用断言功能)
                assert Arrays.equals(calculated, bytes);
            } else {
                // 如果 cp 不在 BMP 上, 则对应2个 code unit(也就是4个byte)
                int high = ((cp - 0x10000) >> 10) + 0xD800;
                int low = ((cp - 0x10000) & 0x3FF) + 0xDC00;

                byte[] calculated = new byte[]{
                        (byte) (high >> 8), (byte) (high & 0xFF),
                        (byte) (low >> 8), (byte) (low & 0xFF)
                };

                // 如果两者不一致, 会抛出异常(注意运行时要开启 -ea 选项才能启用断言功能)
                assert Arrays.equals(calculated, bytes);
            }
        }
    }
}
  1. UTF-8
    utf-8的计算方法

    图片来源
    简述:
    a. 蓝色框中为起始的 code point
    b. 绿色框中为终止的 code point
    c. 红色框中为需要填写的 bit

举个例子
求 UTF-8 对 U+1F602 进行编码的结果
下面的计算过程是在 Python3 中生成的

>>> cp = 0x1F602
>>> b0 = 0b11110000 + ((cp >> 18) & 0b111)
>>> print(hex(b0))
0xf0
>>> b1 = 0b10000000 + ((cp >> 12) & 0b111111)
>>> print(hex(b1))
0x9f
>>> b2 = 0b10000000 + ((cp >> 6) & 0b111111)
>>> print(hex(b2))
0x98
>>> b3 = 0b10000000 + (cp & 0b111111)
>>> print(hex(b3))
0x82
>>> 

所以对 U+1F602 的编码结果为

0xF0
0x9F
0x98
0x82
package com.naive.wow;

import java.io.UnsupportedEncodingException;
import java.util.Arrays;
import java.util.Random;

public class NaiveUTF8 {
    public static void main(String[] args) throws UnsupportedEncodingException {
        Random random = new Random();

        for (int i = 0; i < 100; i++) {
            // 产生一个随机的 code point
            int cp = (int) (random.nextDouble() * 0x10FFFF);

            // U+D800..U+DFFF 上的 code point 不用验证
            if (cp >= 0xD800 && cp <= 0xDFFF) {
                continue;
            }

            String s = new String(new int[]{cp}, 0, 1);
            // bytes 中保存了正确的编码结果
            byte[] bytes = s.getBytes("UTF-8");

            if (cp < 0x80) {
                // U+0000..U+007F: 7个bit 可以表示. 二进制表示形如 0xxxxxxx
                byte[] calculated = new byte[]{
                        (byte) cp
                };

                // 如果两者不一致, 会抛出异常(注意运行时要开启 -ea 选项才能启用断言功能)
                assert Arrays.equals(calculated, bytes);
            } else if (cp < 0x800) {
                // U+0080..U+07FF: 11个bit 可以表示. 二进制表示形如 110xxxxx 10xxxxxx
                byte[] calculated = new byte[]{
                        (byte) (0b11000000 + (cp >> 6)),
                        (byte) (0b10000000 + (cp & 0x3F))
                };

                // 如果两者不一致, 会抛出异常(注意运行时要开启 -ea 选项才能启用断言功能)
                assert Arrays.equals(calculated, bytes);
            } else if (cp < 0x10000) {
                // U+0800..U+FFFF: 16个bit 可以表示. 二进制表示形如 1110xxxx 10xxxxxx 10xxxxxx
                byte[] calculated = new byte[]{
                        (byte) (0b11100000 + (cp >> 12)),
                        (byte) (0b10000000 + ((cp >> 6) & 0x3F)),
                        (byte) (0b10000000 + (cp & 0x3F))
                };

                // 如果两者不一致, 会抛出异常(注意运行时要开启 -ea 选项才能启用断言功能)
                assert Arrays.equals(calculated, bytes);
            } else {
                // U+10000..U+10FFFF: 21个bit 可以表示. 二进制表示形如 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
                byte[] calculated = new byte[]{
                        (byte) (0b11110000 + (cp >> 18)),
                        (byte) (0b10000000 + ((cp >> 12) & 0x3F)),
                        (byte) (0b10000000 + ((cp >> 6) & 0x3F)),
                        (byte) (0b10000000 + (cp & 0x3F))
                };

                // 如果两者不一致, 会抛出异常(注意运行时要开启 -ea 选项才能启用断言功能)
                assert Arrays.equals(calculated, bytes);
            }
        }
    }
}

问题4: 在 Java 中如何遍历 String 里的 code point?

方法1

public class Traverse {
    public static void main(String[] args) {
        // s 中的 code point 数量为 3
        // s 中的 char 数量为 5
        String s = "\uD83d\uDE02" + " " + "\uD83d\uDE02";

        int pos = 0;
        while (pos < s.length()) {
            int cp = s.codePointAt(pos);
            // 如果 cp 在 BMP, 则 pos += 1. 如果 cp 不在 BMP, 则 pos += 2
            pos += Character.isBmpCodePoint(cp) ? 1 : 2;
            System.out.println(Integer.toHexString(cp));
        }
    }
}

方法2 (Java 8 中支持)

public class Traverse {
    public static void main(String[] args) {
        String s = "\uD83d\uDE02" + " " + "\uD83d\uDE02";
        for (int cp : s.codePoints().toArray()) {
            System.out.println(Integer.toHexString(cp));
        }
    }
}

问题5: MySQL 中的 utf8 和 utf8mb4 的区别是什么?

可以参考(MySQL官网) Unicode Support的相关介绍

  1. MySQL 中的 utf8 Character Set 是 utf8mb3 Character Set 的别名, 仅支持 BMP 中的 code point (MySQL官网) The utf8mb3 Character Set (3-Byte UTF-8 Unicode Encoding)
  2. MySQL 中的 utf8mb4 Character Set 支持 BMP 和其他平面的 code point (MySQL官网) The utf8mb4 Character Set (4-Byte UTF-8 Unicode Encoding)

验证

  1. 建表(注意 CHARSET=utf8)
CREATE TABLE `Naive` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增主键',
  `name` varchar(4) NOT NULL COMMENT '名称',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
  1. 执行 insert 语句
insert into Naive(`name`)values("");

执行后会看到报错


insert 时的报错
  1. 查看 utf-8 编码
    在 https://codepoints.net/ 查看 的 utf-8 编码, 与报错信息吻合
    对应的 utf-8 编码
  2. 建立支持非 BMP 字符的表
CREATE TABLE `Good` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增主键',
  `name` varchar(4) NOT NULL COMMENT '名称',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
  1. 执行 insert 语句
insert into Good(`name`)values("");
  1. 确认结果
    执行如下 select 语句
select * from Good;

结果为


select 语句执行结果
  1. more
    可以试试如下的两个 insert 语句
insert into Good(`name`)values("");
insert into Good(`name`)values("");

彩蛋

如何查询不认识的字符

在 https://codepoints.net/ 可以查询字符。
例如若想查询 这个 code point 的相关信息,可以访问https://codepoints.net/,会看到如下的信息(包括 utf-8/utf-16/utf-32 编码的结果以及各种语言中如何表示这个 code point)

相关信息

Plane 0 中的一些字符

: U+2800
可参见 codepoints 网站的相关描述
: U+2F27
可参见 codepoints 网站的相关描述
: U+4DC0
可参见 wikipedia 中的 Yijing Hexagram Symbols
: U+5DED
可参见 codepoints 网站的相关描述

Plane 1 中的一些字符

: U+1F022 (位于 Plane 1)
可参见 codepoints 网站的相关描述
: U+1F0A1 (位于 Plane 1)
可参见 codepoints 网站的相关描述
: U+1F602 (位于 Plane 1)
可参见 codepoints 网站的相关描述

Plane 2 中的一些字符

: U+2000B
可参见 codepoints 网站的相关描述
: U+2F8B2
可参见 [codepoints 网站的相关描述]
(https://codepoints.net/U+2F8B2)
: U+2F9C4
可参见 [codepoints 网站的相关描述]
(https://codepoints.net/U+2F9C4)

其他

3个code point拼接在一起

ಠ_ಠ: 里面有3个code point(2个U+0CA0 ( ), 1个 U+005F(_))

这是1个code point吗?

‍‍‍: 由7个 code point组成(具体如下)

  1. : U+1F468
  2. zero-width joiner (ZWJ): U+200D
  3. : U+0x1F469
  4. zero-width joiner (ZWJ): U+200D
  5. : U+1F467
  6. zero-width joiner (ZWJ): U+200D
  7. : U+1F466

看起来一样?

: U+1F46A
‍‍: 由5个 code point组成(具体如下)

  1. : U+1F468
  2. zero-width joiner (ZWJ): U+200D
  3. : U+0x1F469
  4. zero-width joiner (ZWJ): U+200D
  5. : U+1F466

不同肤色

(Girl): U+1F467
(Girl: Light Skin Tone): U+1F467, U+1F3FB
(Girl: Medium-Light Skin Tone): U+1F467, U+1F3FC
(Girl: Medium Skin Tone): U+1F467, U+1F3FD
(Girl: Medium-Dark Skin Tone): U+1F467, U+1F3FE
(Girl: Dark Skin Tone): U+1F467, U+1F3FF

它们一样吗?

与汉字相似的一些code point

  1. -: U+002D
  2. ˗: U+02D7
  3. : U+2010
  4. : U+2012
  5. : U+2013
  6. : U+2014
  7. : U+2015
    来源

a相似的一些 code point

  1. : U+FF41
  2. : U+1D41A
  3. : U+1D44E
  4. : U+1D482
  5. : U+1D5BA
  6. : U+1D5EE
  7. : U+1D622
    来源

Flags

(China): U+1F1E8, U+1F1F3
(Hong Kong SAR China): U+1F1ED, U+1F1F0
(Macau SAR China): U+1F1F2, U+1F1F4
(United States): U+1F1FA, U+1F1F8

可以用以下26个 code point 来组成 flag
U+1F1E6..U+1F1FF

:U+1F1E6
:U+1F1E7
:U+1F1E8
:U+1F1E9
:U+1F1EA
:U+1F1EB
:U+1F1EC
:U+1F1ED
:U+1F1EE
:U+1F1EF
:U+1F1F0
:U+1F1F1
:U+1F1F2
:U+1F1F3
:U+1F1F4
:U+1F1F5
:U+1F1F6
:U+1F1F7
:U+1F1F8
:U+1F1F9
:U+1F1FA
:U+1F1FB
:U+1F1FC
:U+1F1FD
:U+1F1FE
:U+1F1FF

例如中国为 cn, 将上面的 放在一起就可以看到

动手实战

查看一个字符的 utf-8 编码对应的字节序列

除了可以在 https://codepoints.net/ 上查询外, 也可以自己用 Python3 的程序来做到
下面是 utf8.py

#!/usr/local/bin/python3

import sys

f = open('result', 'wb')
f.write(sys.argv[1].encode('utf-8'))
f.close()

我们在命令行执行

./utf8.py ''

后, 在 utf-8 编码下的对应的字节序列就会输出到 名为 result 的文件中
然后在命令行用od命令可以查看其中的内容(具体如下)

od -t x1 result
查看内容

查看一个字符的 utf-16/utf-32 编码对应的字节序列

而查看 utf-16/utf-32 编码对应的字节序列也是类似的. 下面是对应的 Python3 程序

utf16be.py
#!/usr/local/bin/python3

import sys

f = open('result', 'wb')
f.write(sys.argv[1].encode('utf-16be'))
f.close()

utf32be.py
#!/usr/local/bin/python3

import sys

f = open('result', 'wb')
f.write(sys.argv[1].encode('utf-32be'))
f.close()

你可能感兴趣的:(UTF-8, UTF-16, UTF-32)