3.6.4字元編碼
咦?怎麼好像有東西亂入了?不是講基本資料型別麼?哈哈,因為還剩下最後一個char型了,因為char型會牽涉到Unicode編碼相關,因此我決定先科普一下字符集編碼。
我兒子現在上小學,他們從1年級就開始學英語,為啥啊?因為英語是全球通用語言啊,我就是英語沒學好,現在查資料看到英文版的就頭疼。好像有點扯遠了,言歸正傳,我們人和人之間溝通,需要通過語言,即我們把要表達的意思通過語言文字儲存起來,通過閱讀語言文字就能知道其含義。計算機只認識0和1組成的二進位制串,那麼我們和計算機溝通,就需要解決3個問題:
- 劃分出人類的文字、符號的集合,簡稱字符集
- 把字符集中每一個字元,都定義一個唯一的二進位制編碼與之對應
- 給定一個二進位制串,通過一定規則,解釋出人類的文字
我個人把這3個問題稱之為字元編碼3要素:“字符集”、“編碼”和“解碼”。用一張示意圖表示:
能夠解決這3個問題的規則,就是字元編碼。字元編碼隨著計算機的發展,經歷了一個漫長的過程,下面儘量用簡潔的語言講明字元編碼的簡要發展過程及主要的一些字元編碼方案。
3.6.4.1ASCII、ISO8859-1
在計算機早期,使用者只使用英文,英文字母只有26個,再加上數字、標點符號和一些其他字元也不超過128個,因此用7位的二進位制就可以表示(7位二進位制可以表示27=128個字元)。但是因為計算機都是以位元組為單位,因此規定佔用0-127一共128個8位二進位制碼來表示英文字母、數字、標點符號和一些其他字元。這種編碼方式叫做ASCII編碼(American Standard Code for Information Interchange)。例如大寫字母A的ASCII編碼是0b01000001,十進位制是65。下表列出小部分ACSII編碼:
二進位制 |
十六進位制 |
十進位制 |
符號 |
說明 |
0000 0000 |
00 |
0 |
|
空字元(Null),不可見字元 |
0001 1111 |
1F |
31 |
|
單元分隔符,不可見字元 |
0010 0000 |
20 |
32 |
空格 |
空格 |
0010 0001 |
21 |
33 |
! |
|
0011 0000 |
30 |
48 |
0 |
數字0 |
0011 0001 |
31 |
49 |
1 |
數字1 |
0100 0001 |
41 |
65 |
A |
大寫字母A |
0110 0001 |
61 |
97 |
a |
小寫字母a |
0111 1111 |
7F |
127 |
|
刪除,不可見字元 |
ASCII碼的字元和二進位制碼是一一對應的,因此解碼規則無需多言。
ISO-8859-1(Latin1)編碼是對ASCII的擴充,向下相容ASCII,其編碼範圍是0x00-0xFF,0x00-0x7F之間完全和ASCII一致,0x80-0x9F之間是控制字元,0xA0-0xFF之間是文字元號。因為ISO-8859-1編碼範圍使用了單位元組內的所有空間,在支援ISO-8859-1的系統中傳輸和儲存其他任何編碼的位元組流都不會被拋棄。換言之,把其他任何編碼的位元組流當作ISO-8859-1編碼看待都沒有問題。
示意圖:
3.6.4.2GB2312、GBK、GB18030
隨著計算機普及,問題馬上就來了,要表示一些非英文字母怎麼辦呢?例如中文。為了滿足這種需要,中國國家標準總局釋出了一系列的漢字字符集國家標準編碼,統稱為GB碼,或國標碼。其中最有影響的是於1980年釋出的《資訊交換用漢字編碼字符集 基本集》,標準號為GB 2312-1980,這就是GB2312編碼。 GB 2312是一個簡體中文字符集,由6763個常用漢字和682個全形的非漢字字元組成。GB2312採用了二維矩陣編碼法對所有字元進行編碼。首先構造一個94行94列的方陣,對每一行稱為一個“區”,每一列稱為一個“位”,然後將所有字元依照下表的規律填寫到方陣中。
分割槽 |
說明 |
第01區 |
中文標點、數學符號以及一些特殊字元 |
第02區 |
各種各樣的數學序號 |
第03區 |
全形西文字元 |
第04區 |
日文平假名 |
第05區 |
日文片假名 |
第06區 |
希臘字母表 |
第07區 |
俄文字母表 |
第08區 |
中文拼音字母表 |
第09區 |
製表符號 |
第10-15區 |
無字元 |
第16-55區 |
一級漢字(以拼音字母排序) |
第56-87區 |
二級漢字(以部首筆畫排序) |
第88-94區 |
無字元 |
這樣所有的字元在方陣中都有一個唯一的位置,這個位置可以用區號、位號合成表示,稱為字元的區位碼。如第一個漢字“啊”出現在第16區的第1位上,其區位碼為16 01。這樣所有的字元都可通過其區位碼轉換為數字編碼資訊。實際釋出的國標碼是通過把區位碼都加上32,例如漢字“啊”的國標碼是48 33(16+32,01+32)。一般用十六進位制表示0x3021。至於為什麼不直接釋出區位碼,我也沒查到相關資料,個人猜測是為了避開ASCII碼的控制字元。ASCII碼中0-31和127都是不可見的控制字元,區碼和位碼+32後,範圍就變成32-126,正好避開所有的控制字元。
但是這裡還有個問題,因為國標碼的高、低位元組取值範圍都是在32-126之間,例如漢字‘徠’在GB2312中的國標碼為97 98,而兩個英文字母‘ab’的儲存碼也是97,98。這種衝突將導致在解釋編碼時到底表示的是一個漢字還是兩個英文字元將無法判斷。為避免ASCII碼發生衝突,GB2312字元在進行儲存時不能按照國標碼儲存。我們可以發現國標碼的二進位制最高位都是0,如果我們把每個位元組最高位都變為1來儲存。這樣在解釋編碼時,如果一個位元組最高位為0,則表示西文字元,否則表示GB2312中字元的一個位元組。位元組最高位變為1,只需要將國標碼每個位元組都加上128即可,這個碼叫機內碼。例如漢字‘徠’的區位碼為6566(0x4142),其機內碼為0xE1E2,轉換過程為:
區位碼 |
國標碼 |
高位轉換 |
低位轉換 |
機內碼 |
0x4142 |
0x6162 |
0x61+0x80=E1 |
0x62+0x80=E2 |
0xE1E2 |
其實可以相當於區位碼分別加上160,得到機內碼。
GB2312基本滿足了漢字的計算機處理需要,它所收錄的漢字已經覆蓋中國大陸99.75% 的使用頻率,但是對於人名、古漢語等方面出現的罕用字,GB 2312 不能處理,這導致了後來 GBK 及 GB 18030 漢字字符集的相繼出現。
GBK全稱《漢字內碼擴充套件規範》(GBK即“國標”、“擴充套件”漢語拼音的第一個字母,英文名稱:Chinese Internal Code Specification) ,中華人民共和國全國資訊科技標準化技術委員會1995年12月1日製訂,國家技術監督局標準化司、電子工業部科技與質量監督司1995年12月15日聯合以技監標函1995 229號檔案的形式,將它確定為技術規範指導性檔案。這一版的GBK規範為1.0版。GBK 向下與 GB 2312 編碼相容,是在GB2312-80標準基礎上的內碼擴充套件規範,使用了雙位元組編碼方案,其編碼範圍從8140至FEFE(剔除xx7F),共23940個碼位,共收錄了21003個漢字,完全相容GB2312-80標準,支援國際標準ISO/IEC10646-1和國家標準GB13000-1中的全部中日韓漢字,幷包含了BIG5編碼中的所有漢字。GBK編碼方案於1995年10月制定, 1995年12月正式釋出。GBK其實是一個過渡性的規範,現在已經完成其使命了。但是仍然被廣泛使用。
GB 18030,全稱《資訊科技 中文編碼字符集》,是中華人民共和國國家標準所規定的變長多位元組字符集。其對GB 2312-1980完全向後相容,與GBK基本向後相容,並支援Unicode(GB 13000)的所有碼位。GB 18030共收錄漢字70,244個。
GB18030一共有2個版本:GB18030-2000和GB18030-2005。2000年釋出的GB18030-2000,全名是《資訊科技 漢字編碼字符集 基本集的擴充》。GB18030-2000僅規定了常用非漢字元號和27533個漢字(包括部首、部件等)的編碼。GB18030-2000是全文強制性標準,市場上銷售的產品必須符合。2005年釋出的GB18030-2005在GB18030-2000的基礎上增加了42711個漢字和多種我國少數民族文字的編碼,增加的這些內容是推薦性的。
示意圖如下:
3.6.4.3ANSI編碼
上面我們搞明白了GB2312編碼,它是為了解決中文簡體字元編碼而制定的一種編碼標準。其他國家和地區也相應的制定了他們的標準,例如繁體中文的BIG5,日文的JIS等。這些都是使用 2 個位元組來代表一個字元,人們把他們統稱為 ANSI 編碼,又稱為"MBCS(Muilti-Bytes Character Set,多位元組字符集)"。在ANSi編碼下,同一個編碼值,在不同的編碼體系裡代表著不同的字。在簡體中文系統下,ANSI 編碼代表 GB2312 編碼,在日文作業系統下,ANSI 編碼代表 JIS 編碼,在ANSI編碼體系下,要想開啟一個文字檔案,不但要知道它的編碼方式,還要安裝有對應編碼表,否則就可能無法讀取或出現亂碼。為什麼電子郵件和網頁都經常會出現亂碼,就是因為資訊的提供者可能是日文的ANSI編碼體系,資訊的讀取者可能是中文的編碼體系,他們對同一個二進位制編碼值進行顯示,採用了不同的編碼,導致亂碼。這個問題促使了unicode碼的誕生。如果有一種編碼,將世界上所有的符號都納入其中,無論是英文、日文、還是中文等,大家都使用這個編碼表,就不會出現編碼不匹配現象。每個符號對應一個唯一的編碼,亂碼問題就不存在了。這就是Unicode編碼。
3.6.4.4Unicode字元
Unicode的發展也經歷了一些過程,目前已經有13個版本了。這裡就不再複述。我們只需要知道,Unicode確實做到了將全世界文字元號都統一編碼,在表示一個Unicode的字元時,通常會用“U+”然後緊接著一組十六進位制的數字來表示這一個字元。例如U+0041表示大寫字母A。
目前Unicode的編碼從U+0000到U+10FFFF,一共有1114112個碼位(code point)。然後按照順序分成17個平面(Plane),每個平面包含216=65536個碼位。具體如下:
平面 |
範圍 |
說明 |
Plane0 |
U+0000~U+FFFF |
基本多文種平面(Basic Multilingual Plane, BMP) |
Plane1 |
U+10000~U+1FFFF |
多文種補充平面(Supplementary Multilingual Plane, SMP) 包含古文字,專用文字,符號和特定領域用的標記。古文字諸如埃及象形文字,楔形文字等,現代音樂標記,Emoji表情等都屬於這個平面的範疇 |
Plane2 |
U+20000~U+2FFFF |
表意文字補充平面(Supplementary Ideographic Plane, SIP) 主要對CJK的字元進行補充 |
Plane3 |
U+30000~U+3FFFF |
表意文字第三平面(Tertiary Ideographic Plane, TIP),暫未使用 |
Plane4~Plane13 |
U+40000~U+DFFFF |
未使用(unassigned) |
Plane14 |
U+E0000~U+EFFFF |
特別用途補充平面(Supplementary Special-purpose Plane, SSP)240個(VS17~VS256)補充變數選擇器(Variation Selectors Supplement)就在這個平面定義 |
Plane15~Plane16 |
U+F0000~U+10FFFF |
保留作為私人使用區(Private Use Area, PUA) |
平面0包含了幾乎現代語言的常用字元和大量符號。其中U+D800~U+DFFF這2048個碼位保留作為代理,具體在UTF-16中會闡述。
需要注意的是,Unicode 只是一個字符集,它只規定了符號的二進位制程式碼,卻沒有規定這個二進位制程式碼應該如何儲存。比如,U+0041表示大寫字母A,至少需要1個位元組儲存。U+4E2D表示漢字‘中’,至少需要2個位元組儲存。具體一個字元是用幾個位元組儲存,如何儲存,Unicode並沒有規定。 這就導致了一個問題,計算機在解釋1個位元組的時候,怎麼知道它是表示一個ASCII符號,還是一個其他符號的第一個位元組呢?也就是說,我們得有一個儲存實現來儲存Unicode編碼。目前有UTF-8、UTF-16、UTF-32這幾種方式。示意圖如下:
3.6.4.5UTF-8
UTF-8就是Unicode的一種實現,它把Unicode編碼劃分為不同的範圍,採用一種變長的編碼方式,對於不同範圍採用不同的位元組數來編碼。我們可以用如下表來表示:
Unicode編碼 |
UTF-8儲存碼模板 |
U+0000- U+007F |
0xxxxxxx |
U+0080- U+07FF |
110xxxxx 10xxxxxx |
U+0800- U+FFFF |
1110xxxx 10xxxxxx 10xxxxxx |
U+10000- U+10FFFF |
11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
我們可以看到,Unicode編碼一共被劃分成4個範圍,分別用1-4個位元組來儲存不同範圍的編碼。我們對著這個表,搞清楚2個事情,一是給定Unicode編碼,如何確定UTF-8編碼。二是給定一個UTF-8位元組流,如何確定Unicode編碼:
- 對於一個給定的Unicode編碼,我們可以確定它的範圍,然後確定UTF-8編碼的模板。按照表中把固定的1和0填上,剩下的xxx部分用Unicode編碼補滿即可。
例1:中文“漢”字的Unicode編碼是0x6C49。0x6C49在0x0800-0xFFFF之間,使用3位元組模板:1110xxxx 10xxxxxx 10xxxxxx。x的數量是16。將0x6C49寫成16位二進位制是:0110 1100 0100 1001, 用這個位元流依次代替模板中的x,得到:11100110 10110001 10001001,即E6 B1 89。
例2:Unicode編碼0x20C30在0x010000-0x10FFFF之間,使用4位元組模板:11110xxx 10xxxxxx 10xxxxxx 10xxxxxx。x的數量是21,。將0x20C30寫成21位二進位制數字:0 0010 0000 1100 0011 0000,用這個位元流依次代替模板中的x,得到:11110000 10100000 10110000 10110000,即F0 A0 B0 B0。
- 對於一個給定UTF-8儲存碼,如何知道表示什麼字元?其實很簡單,對於給定的一個位元組,如果第1位是0,則表示是單位元組字元,如果第1位是1,則看連續有幾個1,比如有4個1,就是4位元組字元,再去掉模板中的固定1和0,剩下的拼接到一起,就是Unicode編碼,就知道對應的字元了。
示意圖如下:
3.6.4.6UTF-16
UTF-16是Unicode的另一種實現。我們也搞清楚2個事情,一是給定Unicode編碼,如何確定UTF-16編碼。二是給定一個UTF-16位元組流,如何確定Unicode編碼:
- 對於一個給定的Unicode編碼U,如果是屬於平面0,即U+0000到U+FFFF,把對應的Unicode編碼補足為16位,就是UTF-16編碼。如果U≥U+10000,我們先計算U'=U-0x10000,U'的最大值就是0x10FFFF-0x10000=0xFFFFF。所以U'可以用20個二進位制位表示。我們把U'的二進位制補足位20位,假設是yyyy yyyy yyxx xxxx xxxx,U的UTF-16編碼就是:110110yyyyyyyyyy 110111xxxxxxxxxx。也就是說非0平面的字元,需要用4個位元組表示。
例如:Unicode編碼0x20C30,減去0x10000後,得到0x10C30,寫成二進位制是:0001 0000 1100 0011 0000。用前10位依次替代模板中的y,用後10位依次替代模板中的x,就得到:1101100001000011 1101110000110000,即0xD843 0xDC30。
- 按照上述規則,對於非0平面的字元的UTF-16編碼有4個位元組,第一個16位的高6位是110110,第二個16位的高6位是110111。可見,第一個16位的取值範圍(二進位制)是11011000 00000000到11011011 11111111,即0xD800-0xDBFF。第二個16位的取值範圍(二進位制)是11011100 00000000到11011111 11111111,即0xDC00-0xDFFF。還記得平面0的2048個保留碼位嗎?正好就是0xD800-0xDFFF。這就好辦了。
- 對於一個給定UTF-16位元組流,2個位元組2個位元組讀取,如果這2個位元組不在0xD800-0xDFFF範圍,則是平面0的字元。否則連續讀取4個位元組,把高位2個位元組去掉前6位,把低位2個位元組去掉前6位,然後拼接在一起,再加上0x10000,結果就是Unicode編碼。
示意圖如下:
3.6.4.7字元編碼總結
首先看一個問題,在win10系統下,新建一個記事本,點選“檔案”->“另存為”,彈出儲存框,截圖如下:
一共有5種編碼,我們記事本輸入文字“Java大失叔”,分別儲存為這5種型別,然後用“winHex”軟體開啟,檢視十六進位制編碼。下面分別列出十六進位制編碼及對應的說明:
編碼 |
十六進位制 |
說明 |
ANSI |
4A 61 76 61 B4 F3 CA A7 CA E5 |
在中文簡體Win10下,代表GBK “Java”用4個單位元組表示。漢字“大失叔”分別用2個位元組表示 |
UTF-16 LE |
FF FE 4A 00 61 00 76 00 61 00 27 59 31 59 D4 53 |
UTF-16編碼,其字尾LE 即 little-endian,小端的意思。就是將高位位元組放在前面,文字開頭有2個位元組用來表明位元組序列:FF FE。 無論字母漢字都是2個位元組 |
UTF-16 BE |
FE FF 00 4A 00 61 00 76 00 61 59 27 59 31 53 D4 |
UTF-16編碼,其字尾BE即 big-endian,大端的意思。就是將高位位元組放在後面,文字開頭有2個位元組用來表明位元組序列:FE FF 無論字母漢字都是2個位元組 |
UTF-8 |
4A 61 76 61 E5 A4 A7 E5 A4 B1 E5 8F 94 |
UTF-8編碼,“Java”用4個單位元組表示。漢字“大失叔”分別用3個位元組表示 |
帶有BOM的UTF-8 |
EF BB BF 4A 61 76 61 E5 A4 A7 E5 A4 B1 E5 8F 94 |
BOM(Byte Order Mark),位元組序列的意思。UTF-8編碼本來無需BOM,但是可以用來表明編碼方式。收到位元組流帶有EFBBBF,就知道是UTF-8。 “Java”用4個單位元組表示。漢字“大失叔”分別用3個位元組表示 文字開頭多了EF BB BF 3個位元組 |
最後用一張總結一下:
3.6.5char型
終於把字元編碼搞定了,是不是有點頭昏腦漲了?好吧,接下來來點輕鬆的。我們繼續Java的最後一個基本資料型別char。還記得UTF-16嗎?對於平面0的字元,採用的是2個位元組來表示,我們把2個位元組稱為一個程式碼單元(code unit),char就是用來表示一個程式碼單元,也就是說,char不能表示所有的Unicode字元。
這裡有個小插曲,Unicode是在1991年釋出的1.0,當時都認為16位足以涵蓋所有的字元了,因此Java定義一個2個位元組的char型別來表示所有字元。但是好景不長,Unicode字符集隨後爆炸增長,Java就面臨一個問題了,是把char擴充為4個位元組呢?還是重新定義一個新的型別?考慮到相容性的問題,Java換成了UTF-16編碼,char用來表示一個程式碼單元。
因此,在實際工作和實踐中,儘量避免使用char型別,除非你對所要操作的內容非常熟悉。後面我們講到String類的時候,會繼續詳細分析這一塊內容。
雖然不建議使用char,但是我們還是得了解char的使用,因為你不用,不代表別人不用,我們不學會使用,將來就看不懂別人寫的程式碼。
首先是賦值,我們把一個‘中’賦值給一個char,可以有3種方式:
char a = '中';// 直接用字元的符號賦值 char b = 20013;// 用0~65535的任意十進位制數值賦值,當然二進位制、十六進位制也行 char c = '\u4e2d';// 用Unicode編碼賦值
因為可以把一個數值賦值給char,因此char還可以直接參與運算:
// 中的編碼十進位制是20013,a的編碼十進位制是97 char a = '中' + 'a';// char型別相加,提升為int型別,輸出對應的字元"於" int b = '中' + 'a';// 結果是20110 char c = '中' + 97;// 輸出結果是"於"