參考文章
- http://unicode.org/faq/utf_bom.html
- 深入分析 Java 中的中文編碼問題
- (wikipedia) Plane (Unicode)
- https://codepoints.net/
- (知乎) Unicode字符集中有哪些神奇的字符?
- Java Language Specification 的 3.1 小節(jié)
- (阮一峰) 字符編碼筆記:ASCII,Unicode 和 UTF-8
- (MySQL 官方文檔) 10.9.3 The utf8 Character Set (Alias for utf8mb3)
- Unicode 中有多少個 code point
- ?? Families
- Unicode surrogate programming with the Java language
- (簡書) Unicode和UTF-8、UTF-16、UTF-32
問題
- 什么是 Unicode?
- Unicode 中的 code point 與 Java 中的 char 有何關系?
- utf-8,utf-16,utf-32 是什么?
- 在 Java 中如何遍歷 String 里的 code point?
- MySQL 中的 utf8 和 utf8mb4 的區(qū)別是什么?
重要名詞
-
plane和Basic Multilingual Plane -
code point與code unit -
high surrogate和low 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.
Unicode 中定義了一些自然數(shù)(包括0)和 code point 之間的映射關系
Unicode 中的平面(plane)
在 Unicode 標準中, 每個
plane由65536個 code point 組成. 總共有17個plane, 編號從0到16,Plane 16里最后一個code point是U+10FFFF.Plane 0被稱為 Basic Multilingual Plane (BMP), 其中包含了最常用的code point.Plane 1到Plane 16被稱為 "supplementary planes".[1]

其中4個 plane 的分配情況(白底色表示未分配, 其他底色表示已分配)
-
BMP(Basic Multilingual Plane, 即
Plane 0):U+0000..U+FFFF
A map of the Basic Multilingual Plane. Each numbered box represents 256 code points. -
SMP(Supplementary Multilingual Plane, 即
Plane 1):U+10000..U+1FFFF
A map of the Supplementary Multilingual Plane. Each numbered box represents 256 code points. -
SIP(Supplementary Ideographic Plane 即
Plane 2):U+20000..U+2FFFF
A map of the Supplementary Ideographic Plane. Each numbered box represents 256 code points. -
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+0000toU+10FFFF, using the hexadecimalU+nnotation. Characters whose code points are greater thanU+FFFFare 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+D800toU+DBFF), the second from the low-surrogates range (U+DC00toU+DFFF). For characters in the rangeU+0000toU+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, 具體如下
- 當
code point在U+0000..U+D7FF或U+E000..U+FFFF范圍內(nèi)時, 用 1 個 code unit(或 1個Java中的char)來表示這個code point - 當
code point在U+10000..U+10FFFF范圍內(nèi)時, 用 2 個 code unit(或 2個Java 中的char)來表示這個code point
如何用2個char來表示非BMP的code point:
-
U+10000..U+10FFFF范圍內(nèi)一共有 2^20 個code point(0x10FFFF-0x10000+1=0x10000, 即2^20), 所以用20個bit可以區(qū)分這些code point - 高代理(
high surrogate)有1024種可能取值, 低代理(low surrogate)也有1024種可能取值, 所以高代理和低代理組成的對(high, low)會有1024 * 1024種可能取值(而1024 * 1024 = 2 ^ 20)
高代理和低代理的位置 - 綠色邊框的高代理區(qū)域共有1024個
code point - 藍色邊框的低代理區(qū)域共有1024個
code point
當code point在 U+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)的計算

解釋如下
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)的計算

解釋如下
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?

問題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é)序列

圖片來源
(wikipedia) BOM
- 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);
}
}
}
- 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);
}
}
}
}
- 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的相關介紹
- MySQL 中的 utf8 Character Set 是 utf8mb3 Character Set 的別名, 僅支持 BMP 中的 code point (MySQL官網(wǎng)) The utf8mb3 Character Set (3-Byte UTF-8 Unicode Encoding)
- MySQL 中的 utf8mb4 Character Set 支持 BMP 和其他平面的 code point (MySQL官網(wǎng)) The utf8mb4 Character Set (4-Byte UTF-8 Unicode Encoding)
驗證
- 建表(注意 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
- 執(zhí)行 insert 語句
insert into Naive(`name`)values("??");
執(zhí)行后會看到報錯

- 查看 utf-8 編碼
在 https://codepoints.net/?? 查看??的 utf-8 編碼, 與報錯信息吻合
對應的 utf-8 編碼 - 建立支持非 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
- 執(zhí)行 insert 語句
insert into Good(`name`)values("??");
- 確認結果
執(zhí)行如下 select 語句
select * from Good;
結果為

- 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組成(具體如下)
-
??: U+1F468 - zero-width joiner (ZWJ): U+200D
-
??: U+0x1F469 - zero-width joiner (ZWJ): U+200D
-
??: U+1F467 - zero-width joiner (ZWJ): U+200D
-
??: U+1F466
看起來一樣?
??: U+1F46A
????????: 由5個 code point組成(具體如下)
-
??: U+1F468 - zero-width joiner (ZWJ): U+200D
-
??: U+0x1F469 - zero-width joiner (ZWJ): U+200D
-
??: 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
-
-: U+002D -
?: U+02D7 -
‐: U+2010 -
?: U+2012 -
–: U+2013 -
—: U+2014 -
―: U+2015
來源
與 a相似的一些 code point
-
a: U+FF41 -
??: U+1D41A -
??: U+1D44E -
??: U+1D482 -
??: U+1D5BA -
??: U+1D5EE -
??: 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

查看一個字符的 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()






