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 小節(jié)
  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 的區(qū)別是什么?

重要名詞

  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 中定義了一些自然數(shù)(包括0)和 code point 之間的映射關系

Unicode 中的平面(plane)

Unicode 標準中, 每個 plane65536code 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 范圍內(nèi)時, 用 1 個 code unit(或 1個Java中的 char)來表示這個 code point
  2. code pointU+10000..U+10FFFF 范圍內(nèi)時, 用 2 個 code unit(或 2個Java 中的 char)來表示這個 code point


如何用2個char來表示非BMP的code point:

  1. U+10000..U+10FFFF 范圍內(nèi)一共有 2^20 個 code point(0x10FFFF-0x10000+1=0x10000, 即2^20), 所以用20個bit可以區(qū)分這些 code point
  2. 高代理(high surrogate)有1024種可能取值, 低代理(low surrogate)也有1024種可能取值, 所以高代理和低代理組成的對(high, low)會有 1024 * 1024 種可能取值(而 1024 * 1024 = 2 ^ 20)
    高代理和低代理的位置
  3. 綠色邊框的高代理區(qū)域共有1024個code point
  4. 藍色邊框的低代理區(qū)域共有1024個code point

code pointU+10000..U+10FFFF 范圍內(nèi)時, 對應的 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. 加上高代理區(qū)域的偏移量
    // 計算步驟合在一起: ((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后, 加上低代理區(qū)域的偏移量 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 范圍內(nèi)的 code point)映射為不同的字節(jié)序列

What are some of the differences between the UTFs?

圖片來源
(wikipedia) BOM

  1. UTF-32BE
    code point 對應的整數(shù)用大端法的4個字節(jié)表示即可
    例如用 UTF-32BE 對 U+1F602 進行編碼, 得到的字節(jié)序列為
0x00
0x01
0xF6
0x02



用 Java 來實現(xiàn) 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++) {
            // 產(chǎn)生一個隨機的 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字節(jié)數(shù)(不過 Java 中的 int 本來就是4字節(jié)的), 就可以得到其對應的 UTF-32BE 編碼
            // calculated 中保存了我們自己計算的編碼結果
            byte[] calculated = new byte[]{
                    0, // 第一個字節(jié)中的每一位都是 0
                    (byte) ((cp >> 16) & 0xFF), // 計算第二個字節(jié)
                    (byte) ((cp >> 8) & 0xFF), // 計算第三個字節(jié)
                    (byte) (cp & 0xFF), // 計算第四個字節(jié)
            };

            // 如果兩者不一致, 會拋出異常(注意運行時要開啟 -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++) {
            // 產(chǎn)生一個隨機的 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++) {
            // 產(chǎn)生一個隨機的 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 數(shù)量為 3
        // s 中的 char 數(shù)量為 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 的區(qū)別是什么?

可以參考(MySQL官網(wǎng)) Unicode Support的相關介紹

  1. MySQL 中的 utf8 Character Set 是 utf8mb3 Character Set 的別名, 僅支持 BMP 中的 code point (MySQL官網(wǎng)) The utf8mb3 Character Set (3-Byte UTF-8 Unicode Encoding)
  2. MySQL 中的 utf8mb4 Character Set 支持 BMP 和其他平面的 code point (MySQL官網(wǎng)) 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. 執(zhí)行 insert 語句
insert into Naive(`name`)values("??");

執(zhí)行后會看到報錯


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. 執(zhí)行 insert 語句
insert into Good(`name`)values("??");
  1. 確認結果
    執(zhí)行如下 select 語句
select * from Good;

結果為


select 語句執(zhí)行結果
  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 網(wǎng)站的相關描述
?: U+2F27
可參見 codepoints 網(wǎng)站的相關描述
?: U+4DC0
可參見 wikipedia 中的 Yijing Hexagram Symbols
: U+5DED
可參見 codepoints 網(wǎng)站的相關描述

Plane 1 中的一些字符

??: U+1F022 (位于 Plane 1)
可參見 codepoints 網(wǎng)站的相關描述
??: U+1F0A1 (位于 Plane 1)
可參見 codepoints 網(wǎng)站的相關描述
??: U+1F602 (位于 Plane 1)
可參見 codepoints 網(wǎng)站的相關描述

Plane 2 中的一些字符

??: U+2000B
可參見 codepoints 網(wǎng)站的相關描述
??: U+2F8B2
可參見 [codepoints 網(wǎng)站的相關描述]
(https://codepoints.net/U+2F8B2)
??: U+2F9C4
可參見 [codepoints 網(wǎng)站的相關描述]
(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, 將上面的 ???? 放在一起就可以看到 ????

動手實戰(zhàn)

查看一個字符的 utf-8 編碼對應的字節(jié)序列

除了可以在 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()

我們在命令行執(zhí)行

./utf8.py '??'

后, ?? 在 utf-8 編碼下的對應的字節(jié)序列就會輸出到 名為 result 的文件中
然后在命令行用od命令可以查看其中的內(nèi)容(具體如下)

od -t x1 result
查看內(nèi)容

查看一個字符的 utf-16/utf-32 編碼對應的字節(jié)序列

而查看 utf-16/utf-32 編碼對應的字節(jié)序列也是類似的. 下面是對應的 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()

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

  • 從這里開始,從這里散場。 同樣的角度,那年冬天有雪。 看著背影,自己還是很高的 我沒去過的景山公園,你們陪我又走一...
    斌心依舊閱讀 412評論 0 0
  • 剛畢業(yè)那會兒,找一個合適的住處真的是難于上青天??!由于漢子這個屬性,很多合適的房子都因為【僅限女生】而把我拒之門外...
    運營獅訓練營閱讀 1,456評論 0 0
  • 電影圍繞關系、如何解決問題展開,接地氣、生活化,為我們家庭生活關系相處提出了警醒。女主人公李寶莉,勤快、潑辣、勇敢...
    云朵兒_e6cb閱讀 454評論 0 0

友情鏈接更多精彩內(nèi)容