
點擊上方藍字關注我們!
作者介紹
王云靜,Java 開發工程師,2018 年 7 月加入去哪兒網,目前在目的地 - 呼叫中心。曾獲得過 ACM 亞洲區域賽銅牌。
-----
基本概念
字符集
字符(Character)是各種文字和符號的總稱,包括各國家文字、標點符號、圖形符號、數字等。字符集(Character set)是多個字符的集合,字符集種類較多,每個字符集包含的字符個數不同,常見字符集名稱:ASCII 字符集、GB2312 字符集、BIG5 字符集、 GB18030 字符集、Unicode 字符集等。計算機要準確的處理各種字符集文字,就需要進行字符編碼,以便計算機能夠識別和存儲各種文字。
字符編碼
字符編碼(英語:Character encoding)也稱字集碼,是把字符集中的字符編碼為指定集合中某一對象(例如:比特模式、自然數序列、8 位組或者電脈沖),以便文本在計算機中存儲和通過通信網絡的傳遞。
為了理解字符集和字符編碼的關系,這里舉個簡單的例子,我們可以把字符集當成接口,把字符編碼當成接口的實現。Unicode
是接口(字符集), UTF-8
/ UTF-16
/ UTF-32
則是不同的實現(字符編碼)。
ANSI
ANSI 全稱(American National Standard Institite)美國國家標準學會(美國的一個非營利組織),首先 ANSI 不是指的一種特定的編碼,而是不同地區擴展編碼方式的統稱,各個國家和地區所獨立制定的兼容 ASCII 但互相不兼容的字符編碼,微軟統稱為 ANSI 編碼。
在簡體中文 windows 下使用文本文件保存”聯通“,則再次打開會顯示亂碼。這是因為 windows 下的文本文件默認使用 ansi 字符集,而簡體中文 windows 下的 ansi 字符集為 GB2312,”聯通“兩個字的 GB2312 編碼看起來和 UTF-8 非常相似,又因為我們沒有在文件開頭設置字符集標記(BOM),所以當我們再次打開該文件時,被識別為 UTF-8,因此出現亂碼。
Unicode
Unicode(統一碼、萬國碼、單一碼)是計算機科學領域里的一項業界標準,包括字符集、編碼方案等。Unicode 是為了解決傳統的字符編碼方案的局限而產生的,它為每種語言中的每個字符設定了統一并且唯一的二進制編碼,以滿足跨語言、跨平臺進行文本轉換、處理的要求。
CodeUnit
:代碼單元/編碼單元,是 Unicode 編碼里一個?CodePoint
需要的最少字節數例如:UTF-8 是一個字節,UTF-16 是兩個字節,UTF-32 是四個字節
CodePoint
:代碼點,Unicode 規定的每一個字符就是一個?CodePoint
CodeSpace
:代碼空間,所有的代碼點構成一個代碼空間,根據 Unicode 定義,總共有?1,114,112
?個代碼點,編號從?0x0-0x10FFFF
,也就是大概 110 多萬個字符CodePlane
:代碼平面,Unicode 標準把代碼點分成了17 個代碼平面,編號為 #0-#16。每個代碼平面包含 65,536(2^16)個代碼點(17*65,536=1,114,112)。#0 叫做?基本多語言平面
(BMP:大部分常用的字符都坐落在這個平面內,比如 ASCII 字符,漢字等。代碼點范圍:0x0000-0xFFFF),其余平面叫做?補充平面
SurrogatePair
:代理對,由一個?High-surrogate
(高代理代碼點:?0xD800-0xDBFF
)和一個?Low-surrogate
(低代理代碼點:?0xDC00-0xDFFF
)組成。這 2048 個代碼點位于 BMP 內,并且不是有效的字符代碼點,它們是為 UTF 編碼保留的。在 UTF-16 中它可以編碼 BMP 之外的代碼點
UTF-8
UTF-8 的特點是對不同范圍的字符使用不同長度的編碼。對于 0x00-0x7F 之間的字符,UTF-8 編碼與 ASCII 編碼完全相同。UTF-8 編碼的最大長度是 4 個字節。
對于單個字節的字符,第一位設為 0,后面的 7 位對應這個字符的 Unicode 碼點。
對于需要使用 N 個字節來表示的字符(N > 1),第一個字節的前 N 位都設為 1,第 N + 1 位設為0,剩余的 N - 1 個字節的前兩位都設位 10,剩下的二進制位則使用這個字符的 Unicode 碼點來填充。
Unicode 編碼(十六進制) | UTF-8 字節流(二進制) |
---|---|
000000-00007F | 0xxxxxxx |
000080-0007FF | 110xxxxx 10xxxxxx |
000800-00FFFF | 1110xxxx 10xxxxxx 10xxxxxx |
010000-10FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
UTF-8 編碼的最大長度是 4 個字節。從上表可以看出,4 字節模板有 21 個 x,即可以容納 21 位二進制數字。Unicode 的最大碼位 0x10FFFF 也只有 21 位。
例1:“漢”字的Unicode 編碼是 0x6C49。0x6C49 在 0x0800-0xFFFF 之間,使用3字節模板:1110xxxx10xxxxxx10xxxxxx
。將0x6C49寫成二進制是:0110110001001001
, 用這個比特流依次代替模板中的x,得到:111001101011000110001001
,即 E6 B1 89。
例2:Unicode編碼0x20C30在0x010000-0x10FFFF 之間,使用 4 字節模板 11110xxx10xxxxxx10xxxxxx10xxxxxx
。將 0x20C30 寫成 21 位二進制數字(不足 21 位就在前面補 0):000100000110000110000
,用這個比特流依次代替模板中的 x,得到:11110000101000001011000010110000
,即 F0 A0 B0 B0。
UTF-16
UTF-16 是 Unicode 的一種編碼方式,它用兩個字節來編碼 BMP 里的代碼點,用四個字節編碼其余平面里的代碼點。
為了書寫方便,我們把 Unicode 編碼記作 U。
如果?
U<0x10000
,U 的 UTF-16 編碼就是 U 對應的 16 位無符號整數。如果?
U≥0x10000
,我們先計算 U'=U-0x10000,然后將 U' 寫成二進制形式:?yyyy yyyy yyxx xxxx xxxx
,U 的 UTF-16 編碼(二進制)就是:?110110yyyyyyyyyy110111xxxxxxxxxx
。
為什么 U' 可以被寫成 20 個二進制位?
Unicode 的最大碼位是 0x10FFFF,減去 0x10000 后,U 的最大值是 0xFFFFF,所以肯定可以用 20 個二進制位表示。
為什么把 0x10000-0x10FFFF
編碼為 110110yyyyyyyyyy110111xxxxxxxxxx
?
110110yyyyyyyyyy
的取值范圍為1101100000000000
-1101101111111111
,即? 0xD800-0xDBFF(High-surrogate
)
110111xxxxxxxxxx
的取值范圍為1101110000000000
-1101111111111111
,即? 0xDC00-0xDFFF(Low-surrogate
) 所以110110yyyyyyyyyy110111xxxxxxxxxx
正好是一個SurrogatePair
,也就是 4 個字節
例如:Unicode 編碼 0x20C30,減去 0x10000 后,得到 0x10C30,寫成二進制是:00010000110000110000
。用前 10 位依次替代模板中的 y,用后 10 位依次替代模板中的 x,就 得到:11011000010000111101110000110000
,即 0xD843 0xDC30。
UTF-32
UTF-32 編碼以 32 位無符號整數為單位。Unicode 的 UTF-32 編碼就是其對應的 32 位無符號整數。
大/小端
大小端是 CPU 處理多字節數的不同方式,其主要特點是字節序在內存中存儲位置不同。
大端(Big-Endian):高字節序存儲在低地址,低字節序存儲在高地址。
小端(Little-Endian):高字節序存儲在高地址,低字節序存儲在低地址。
對于任何字符編碼,編碼單元的順序是由編碼方案指定的,與 endian 無關。例如 GBK 的編碼單元是字節,用兩個字節表示一個漢字。這兩個字節的順序是固定的,不受 CPU 字節序的影響。UTF-16 的編碼單元是 word(雙字節),word 之間的順序是編碼方案指定的,word 內部的字節排列才會受到 endian 的影響。
在網絡上傳輸數據時,由于數據傳輸的兩端對應不同的硬件平臺,采用的存儲字節順序可能不一致。所以在 TCP/IP 協議規定了在網絡上必須采用網絡字節順序,也就是大端模式。
JVM 屏蔽了大小端問題,默認為大端,并且可使用 ByteOrder.nativeOrder()
查詢處理器和內存系統的大小端,在使用 ByteBuffer 時,也可以使用 ByteBuffer.order()
進行設置。
BOM
Unicode 的學名是 "Universal Multiple-Octet Coded Character Set",簡稱為 UCS。UCS 可以看作是 "Unicode Character Set" 的縮寫。在 UCS 編碼中有一個叫做?"Zero Width No-Break Space",中文譯名作“零寬無間斷間隔”的字符,它的編碼是? FEFF。而 FFFE 在 UCS 中是不存在的字符,所以不應該出現在實際傳輸中。UCS 規范建議我們在傳輸字節流前,先傳輸字符 "Zero Width No-Break Space"。這樣如果接收者收到 FEFF,就表明這個字節流是 Big-Endian 的;如果收到 FFFE,就表明這個字節流是 Little- Endian 的。因此字符 "Zero Width No-Break Space" (“零寬無間斷間隔”)又被稱作 BOM(即 Byte Order Mark)。
UTF-8 BOM:UTF-8 以字節為編碼單元,沒有字節序的問題,但可以用 BOM 來表明編碼方式。字符 "Zero Width No-Break Space" 的 UTF-8 編碼是 EF BB BF
。(windows 系統默認使用 UTF-8 BOM 編碼,需要注意)
文件:D:\bom.txt,編碼:UTF-8-BOM
代碼:
public static void main(String[] args) throws Exception {
File file = new File("D:\\\\bom.txt");
FileInputStream inputStream = new FileInputStream(file);
int i = 0;
while ((i = inputStream.read()) != -1) {
System.out.printf("0x"+Integer.toHexString(i) + " ");
}
}
結果
0xef0xbb0xbf0xe60xb50x8b0xe80xaf0x950x550x540x460x380x200x420x4f0x4d
常見BOM
BOM Encoding | 字符編碼 |
---|---|
EF BB BF | UTF-8 BOM |
FE FF | UTF-16 (big-endian) |
FF FE | UTF-16 (little-endian) |
00 00 FE FF | UTF-32 (big-endian) |
FF FE 00 00 | UTF-32 (little-endian) |
Java 字符編碼
Java 使用 Unicode 字符集并且使用 UTF-16 字符編碼。
Java 語言規范規定,Java 的 char 類型是 UTF-16的code unit,也就是一定是 16 位(2 字節)。
一個字符到底占用多少個字節?
對于 Java 中的 char 類型來說的話,固定占用 2 字節,但是為什么使用
newString("字").getBytes().length
返回的是 3,這是因為getBytes
實際是做了編碼轉換
(內碼轉外碼),你可以顯式傳入一個參數來指定編碼,否則它會使用缺省編碼
來轉換。對于肉眼可見的字符來說,這個取決于字符編碼,同一個字符在不同的編碼下占用不同的字節。例如:漢字的"字","字"在 GBK 編碼下占 2 字節,在 UTF-8 編碼下占 3 字節,在 UTF-32 編碼下占 4 字節。
內碼 & 外碼
內碼
:程序內部使用的字符編碼,特別是某種語言實現其 char 或 String 類型在內存里用的內部編碼。Java 的內碼就是 UTF-16。外碼
:程序與外部交互時外部使用的字符編碼。簡單的來說就是除了內碼都可以認為是“外碼”(包括 class 文件的編碼)。
內碼轉外碼:
String.getBytes(StringcharsetName)
:將內存中的字符串用 UTF-16 編碼轉換為指定編碼的 byte 序列。String.getBytes()
:將內存中的字符串用 UTF-16 編碼轉換為缺省編碼的 byte 序列。
外碼轉內碼:
newString(byte[]bytes,Stringcharset)
:就是把字節流以指定的編碼轉換為 UTF-16 編碼的字節流存入內存中。Java 的 class 文件是以 UTF-8 的方式來編碼的。JVM 讀取 class 文件時需要把 UTF-8 編碼轉換為 UTF-16 編碼讀入內存。
注意:編碼和解碼的“字符編碼”必須要一致才能解碼成想要的字符串。
缺省編碼
可以在啟動 JVM 時通過 -Dfile.encoding=UTF-8 來設置,否則使用操作系統環境下的缺省編碼,可通過
Charset.defaultCharset()
獲取編碼。通常,Windows 系統下是 GBK,Linux 和 Mac 是 UTF-8。
如果使用 IDE,則會使用工程的缺省編碼,具體編碼是什么,需要看具體使用的 IDE,可以百度來修改 IDE 的編碼。(有可能會遇到某 IDE 在啟動項目時控制臺打印的日志是亂碼,這就是編碼搞得鬼) 因為
getBytes
受缺省編碼的影響而得到的結果不同,所以在使用該方法時,建議顯示指定編碼。
String#length()獲取的是真正的字符串長度嗎?
不是,它獲取的僅僅是代碼單元的數量,而真正的字符串長度是代碼點數量,可使用
String#codePointCount()
來獲取。原因:我們在上面介紹過,某字符的代碼點>=0x10000時UTF-16 編碼會占用 4 個字節,又因為 Java 的 char 固定就是 2 字節,所以我們需要使用 2 個 char 來表示該字符,那么在使用 String#length()獲取字符串長度時,就會出錯。
為什么我們平時都是使用該方法獲取字符串長度呢?因為 BMP 里定義了我們使用的大部分字符,并且我們基本使用不到 BMP 之外的字符。
為什么 Java 不使用定長編碼呢?
Java 設計之初 UTF-16 確實是定長編碼,只不過后來 Unicode 的字符變多了之后,UTF-16 變成了變長編碼。
Java 5.0 版本既要支持 Unicode 4.0 同時要保證向后兼容性,不得不開始使用 UTF-16 ?作為內部編碼方式。
代碼點/代碼單元分析
代碼
// 16進制,10進制,2進制
// 0xd801, 55297, 1101 1000 0000 0001
// 0xdc01, 56321, 1101 1100 0000 0001
// UTF-16編碼: 0xDB01 0xDC01, 3624000513, 110110 00 0000 0001 110111 00 0000 0001
// U': 0x401, 1025, 00 0000 0001 00 0000 0001
// Unicode碼點 = U'+0x10000: 0x10401, 66561, 0000 0000 0000 0001 0000 0100 0000 0001
String str = "\ud801\udc01";
System.out.println("str = "+ str);
// 代碼單元(char)
System.out.println("str.length() = "+ str.length());
// 代碼點
System.out.println("str.codePointCount(0, str.length()) = "+ str.codePointCount(0, str.length()));
// 代碼點的int值,如果當前位置是高代理,則返回高代理+低代理對應代碼點的int值,否則,會返回當前位置的int值
// 高代理,返回高代理+低代理對應代碼點的int值
System.out.println("str.codePointAt(0) = "+str.codePointAt(0));
// 低代理,返回該位置的int值
System.out.println("str.codePointAt(1) = "+ str.codePointAt(1));
// char,高代理
System.out.println("str.charAt(0) = "+ str.charAt(0));
// char, 低代理
System.out.println("str.charAt(1) = "+ str.charAt(1));
結果
str = ?
str.length() = 2
str.codePointCount(0, str.length()) = 1
str.codePointAt(0) = 66561
str.codePointAt(1) = 56321
str.charAt(0) = ?
str.charAt(1) = ?
編解碼
可通過 Charset.availableCharsets().keySet()來查看 Java 到底支持哪些字符編碼。
編碼:字符集的字符 => 字節數組
解碼:字節數組 => 字符集的字符
編碼轉換:字符集1的字符 => 字符集 2 的字符,通過這里可以看出如果 2 個字符集的某個字符沒有對應關系,那么就會導致亂碼
其他來源的字符保存到 JVM 內存需要經歷:字節數組 => 指定編碼的字符 => Unicode 字符 => UTF-16 編碼后的字節數組。
JVM 內存保存到其他地方需要經歷:UTF-16 編碼后的字節數組 => Unicode字符 => 指定編碼的字符 => 字節數組。
由此可以看出,系統內部是做了一步字符集轉換。如果兩個字符集沒有轉換規則,那么就會使用 Codepage(代碼頁),有興趣的同學可以自行去了解代碼頁。
代碼
public static void main(String[] args) throws Exception {
byte[] utf8Bytes = "編碼轉換".getBytes("UTF-8");
System.out.println("UTF-8 = " + new String(utf8Bytes, "UTF-8"));
System.out.printf("utf8Bytes = ");
print(utf8Bytes);
String gbk = new String(utf8Bytes, "GBK");
System.out.println("GBK = " + gbk);
byte[] gbkBytes = gbk.getBytes("GBK");
System.out.printf("gbkBytes = ");
print(gbkBytes);
byte[] isoBytes = gbk.getBytes("ISO-8859-1");
System.out.printf("isoBytes = ");
print(isoBytes);
System.out.println("UTF-8 = " + new String(gbkBytes, "UTF-8"));
}
private static void print(byte[] bytes) {
for (int i = 0; i < bytes.length; i++) {
System.out.printf(bytes[i] + " ");
}
System.out.println();
}
結果
UTF-8= 編碼轉換
utf8Bytes = -25-68-106-25-96-127-24-67-84-26-115-94
GBK = 緙栫爜杞崲
gbkBytes = -25-68-106-25-96-127-24-67-84-26-115-94
isoBytes = 636363636363
UTF-8= 編碼轉換
1. "編碼轉換"以?UTF-8
編碼存儲在class文件。
2. JVM 加載 class 文件,從常量池中讀取”編碼轉換“,并使用 UTF-8 解碼為 Unicode 字符,然后使用 UTF-16 編碼保存到 JVM 內存。
3. "編碼轉換".getBytes("UTF-8"),直接從 JVM 內存獲取"編碼轉換"的字節數組,然后使用 UTF-16 解碼為 Unicode 字符,最后使用 UTF-8 編碼為新的字節數組。(UTF-16 => UTF-8)
4. new String(utf8Bytes, "GBK"),使用 GBK 把 bytes 解碼為 GBK 字符,然后把 GBK 字符轉換為 Unicode 字符,最后使用 UTF-16 編碼保存到 JVM 內存(GBK 和 Unicode 無聯系,所以通過代碼頁來完成轉換)。
瀏覽器/Tomcat/Mysql
瀏覽器編碼
URI:不同瀏覽器采用的編碼方案不同。例如 chrome 使用 UTF-8 header:ISO-8859-1 body:根據 Content-Type 來進行編碼
chrome瀏覽器
瀏覽器解碼
header:ISO-8859-1 body:首先查看 Content-Type 中是否存在編碼方案,其次若返回的是 html 格式,則查看 meta 中是否指定字符集,最后使用瀏覽器默認字符集解碼
在 chrome 中可以使用插件手動修改默認的字符集。例如:Set Character Encoding。
tomcat 編碼
依賴于應用程序
tomcat 解碼
依賴于 server.xml 的 Connector 的配置
UTIEncoding:URI 的編碼,tomcat7 默認 ISO-8859-1,tomcat8 默認 UTF-8。
useBodyEncodingForURI:使 URI 編碼等于 request.setCharacterEncoding()
設置的編碼。
request.setCharacterEncoding():指定 body 體的編碼方式,必須在第一次獲取 body 體內容之前設置。
Spring
如果使用 Spring MVC,則需要配置 CharacterEncodingFilter
為第一個過濾器,并且指定編碼。
如果使用 Spring Boot,則無需配置,默認配置了 OrderedCharacterEncodingFilter
,默認編碼為 UTF-8。
這個過濾器只是針對的 body 體,至于 get 請求還依賴于使用的 tomcat 版本,如使用 Tomcat7,則需要去 server.xml 配置 UTIEncoding/useBodyEncodingForURI,若使用 Tomcat8,無需配置。
Mysql
character_set_client
:客戶端數據解析、編碼的字符集?
character_set_connection
:連接層字符集
character_set_database
:當前數據庫的字符集
character_set_server
:服務器內部操作字符集?
character_set_results
:查詢結果字符集?
character_set_system
:系統源數據(字段名等)字符集
setnames utf8mb4
等同于同時設置 character_set_client
, character_set_connection
, character_set_results
這三個字符集
1. 客戶端使用特定字符集編碼 SQL 發送到服務端。
2. 服務端接受到字節流后,使用?character_set_client
進行解碼。
3. 服務端解碼后,會使用?character_set_connection
進行編碼,然后傳給存儲引擎。若?character_set_connection
和?character_set_client
不同,則發生字符集轉換操作。
4. 存儲引擎查詢表時,若表字符集和?character_set_connection
不同,則發生字符集轉換操作。
5. 存儲引擎查詢到結果之后,會使用?character_set_results
進行編碼返回給客戶端,若表字符集和?character_set_results
不同,則發生字符集轉換操作。
6. 客戶端接受到結果后,使用特定字符集進行解碼。
客戶端 | charactersetclient | charactersetconnection | 表字符集 | charactersetresults | 結果 |
---|---|---|---|---|---|
utf8 | utf8 | utf8/gbk | utf8/gbk | utf8 | 正常 |
gbk | gbk | utf8/gbk | utf8/gbk | gbk | 正常 |
gbk | utf8 | utf8/gbk | utf8/gbk | gbk | 亂碼 |
gbk | gbk | utf8/gbk | utf8/gbk | utf8 | 亂碼 |
gbk | utf8 | utf8/gbk | utf8/gbk | utf8 | 亂碼 |
由此表可以看出,必須保證 客戶端
、 character_set_client
、 character_set_results
一致才可以保證數據正常。如果使 character_set_connection
、 表字符集
和上面 3 個保持一致,可以減少字符集轉換。
Connector / J
在使用Connector / J時,創建連接語句 jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf8
基本都會配置 useUnicode
和 characterEncoding
。否則創建連接時,驅動程序會自動檢測 character_set_server
并使用該字符集。
要覆蓋客戶端自動檢測到的編碼,請使用 characterEncoding 服務器連接 URL 中的屬性。指定字符編碼時,請使用 Java 樣式的名稱。
對于 Connector / J 5.1.46 及更早版本:為了使用 utf8mb4 字符集進行連接,服務器必須配置為 charactersetserver=utf8mb4。如果不是這種情況,UTF-8則characterEncoding在連接字符串中使用時,它將映射到 MySQL 字符集名稱 utf8。
對于 Connector / J 5.1.47 及更高版本:當 UTF-8 用于 characterEncoding 連接字符串中,它映射到 MySQL 的字符集的名字 utf8mb4。
參考資料
https://www.cnblogs.com/binarylei/p/10760233.html
https://baike.baidu.com/item/Unicode/750500?fr=aladdin
https://baike.baidu.com/item/%E5%AD%97%E7%AC%A6%E9%9B%86/946585?fr=aladdin
https://baike.baidu.com/item/%E5%AD%97%E7%AC%A6%E7%BC%96%E7%A0%81/8446880
https://www.zhihu.com/question/27562173/answer/76208352
http://www.imooc.com/article/26166
http://www.fmddlmyy.cn/text6.html
https://blog.csdn.net/duduniao999/article/details/80872701
https://www.cnblogs.com/lanhaicode/p/11214827.html
https://www.zhihu.com/question/30945431/answer/50046808
https://www.cnblogs.com/jave1ove/p/7454966.html
https://jingyan.baidu.com/article/148a1921189b234d71c3b1df.html
https://dev.mysql.com/doc/connector-j/5.1/en/connector-j-reference-charsets.html
【END】