《Java從入門到失業》第三章:基礎語法及基本程序結構(3.6):基本數據類型及字符集編碼(字符編碼和char型)


3.6.4字符編碼

       咦?怎么好像有東西亂入了?不是講基本數據類型么?哈哈,因為還剩下最后一個char型了,因為char型會牽涉到Unicode編碼相關,因此我決定先科普一下字符集編碼。

       我兒子現在上小學,他們從1年級就開始學英語,為啥啊?因為英語是全球通用語言啊,我就是英語沒學好,現在查資料看到英文版的就頭疼。好像有點扯遠了,言歸正傳,我們人和人之間溝通,需要通過語言,即我們把要表達的意思通過語言文字保存起來,通過閱讀語言文字就能知道其含義。計算機只認識0和1組成的二進制串,那么我們和計算機溝通,就需要解決3個問題:

  1. 划分出人類的文字、符號的集合,簡稱字符集
  2. 把字符集中每一個字符,都定義一個唯一的二進制編碼與之對應
  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;// 輸出結果是"於"


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM