Java的char使用的編碼UTF-16
簡介
編碼問題一直困擾着開發人員,尤其在 Java 中更加明顯,因為 Java 是跨平台語言,不同平台之間編碼之間的切換較多。本文將向你詳細介紹 Java 中編碼問題出現的根本原因,你將了解到:Java 中經常遇到的幾種編碼格式的區別;Java 中經常需要編碼的場景;出現中文問題的原因分析;在開發 Java web 程序時可能會存在編碼的幾個地方,一個 HTTP 請求怎么控制編碼格式?如何避免出現中文問題?
為什么要編碼
不知道大家有沒有想過一個問題,那就是為什么要編碼?我們能不能不編碼?要回答這個問題必須要回到計算機是如何表示我們人類能夠理解的符號的,這些符號也就是我們人類使用的語言。由於人類的語言有太多,因而表示這些語言的符號太多,無法用計算機中一個基本的存儲單元—— byte 來表示,因而必須要經過拆分或一些翻譯工作,才能讓計算機能理解。我們可以把計算機能夠理解的語言假定為英語,其它語言要能夠在計算機中使用必須經過一次翻譯,把它翻譯成英語。這個翻譯的過程就是編碼。所以可以想象只要不是說英語的國家要能夠使用計算機就必須要經過編碼。這看起來有些霸道,但是這就是現狀,這也和我們國家現在在大力推廣漢語一樣,希望其它國家都會說漢語,以后其它的語言都翻譯成漢語,我們可以把計算機中存儲信息的最小單位改成漢字,這樣我們就不存在編碼問題了。
所以總的來說,編碼的原因可以總結為:
計算機中存儲信息的最小單元是一個字節即 8 個 bit,所以能表示的字符范圍是 0~255 個
人類要表示的符號太多,無法用一個字節來完全表示
要解決這個矛盾必須需要一個新的數據結構 char,從 char 到 byte 必須編碼
如何“翻譯”
明白了各種語言需要交流,經過翻譯是必要的,那又如何來翻譯呢?計算中提拱了多種翻譯方式,常見的有 ASCII、ISO-8859-1、GB2312、GBK、UTF-8、UTF-16 等。它們都可以被看作為字典,它們規定了轉化的規則,按照這個規則就可以讓計算機正確的表示我們的字符。目前的編碼格式很多,例如 GB2312、GBK、UTF-8、UTF-16 這幾種格式都可以表示一個漢字,那我們到底選擇哪種編碼格式來存儲漢字呢?這就要考慮到其它因素了,是存儲空間重要還是編碼的效率重要。根據這些因素來正確選擇編碼格式,下面簡要介紹一下這幾種編碼格式。
ASCII 碼
學過計算機的人都知道 ASCII 碼,總共有 128 個,用一個字節的低 7 位表示,0~31 是控制字符如換行回車刪除等;32~126 是打印字符,可以通過鍵盤輸入並且能夠顯示出來。
ISO-8859-1(擴展ASCII編碼)
128 個字符顯然是不夠用的,於是 ISO 組織在 ASCII 碼基礎上又制定了一些列標准用來擴展 ASCII 編碼,它們是 ISO-8859-1~ISO-8859-15,其中 ISO-8859-1 涵蓋了大多數西歐語言字符,所有應用的最廣泛。ISO-8859-1 仍然是單字節編碼,它總共能表示 256 個字符。
GB2312
它的全稱是《信息交換用漢字編碼字符集 基本集》,它是雙字節編碼,總的編碼范圍是 A1-F7,其中從 A1-A9 是符號區,總共包含 682 個符號,從 B0-F7 是漢字區,包含 6763 個漢字。
GBK(擴展GB2312)
全稱叫《漢字內碼擴展規范》,是國家技術監督局為 windows95 所制定的新的漢字內碼規范,它的出現是為了擴展 GB2312,加入更多的漢字,它的編碼范圍是 8140~FEFE(去掉 XX7F)總共有 23940 個碼位,它能表示 21003 個漢字,它的編碼是和 GB2312 兼容的,也就是說用 GB2312 編碼的漢字可以用 GBK 來解碼,並且不會有亂碼。
GB18030(兼容GB2312)
全稱是《信息交換用漢字編碼字符集》,是我國的強制標准,它可能是單字節、雙字節或者四字節編碼,它的編碼與 GB2312 編碼兼容,這個雖然是國家標准,但是實際應用系統中使用的並不廣泛。
Unicode編碼集
ISO 試圖想創建一個全新的超語言字典,世界上所有的語言都可以通過這本字典來相互翻譯。可想而知這個字典是多么的復雜,關於 Unicode 的詳細規范可以參考相應文檔。Unicode 是 Java 和 XML 的基礎,下面詳細介紹 Unicode 在計算機中的存儲形式。
UTF-16
UTF-16 具體定義了 Unicode 字符在計算機中存取方法。UTF-16 用兩個字節來表示 Unicode 轉化格式,這個是定長的表示方法,不論什么字符都可以用兩個字節表示,兩個字節是 16 個 bit,所以叫 UTF-16。UTF-16 表示字符非常方便,每兩個字節表示一個字符,這個在字符串操作時就大大簡化了操作,這也是 Java 以 UTF-16 作為內存的字符存儲格式的一個很重要的原因。
UTF-8
UTF-16 統一采用兩個字節表示一個字符,雖然在表示上非常簡單方便,但是也有其缺點,有很大一部分字符用一個字節就可以表示的現在要兩個字節表示,存儲空間放大了一倍,在現在的網絡帶寬還非常有限的今天,這樣會增大網絡傳輸的流量,而且也沒必要。而 UTF-8 采用了一種變長技術,每個編碼區域有不同的字碼長度。不同類型的字符可以是由 1~6 個字節組成。
UTF-8 有以下編碼規則:
如果一個字節,最高位(第 8 位)為 0,表示這是一個 ASCII 字符(00 - 7F)。可見,所有 ASCII 編碼已經是 UTF-8 了。
如果一個字節,以 11 開頭,連續的 1 的個數暗示這個字符的字節數,例如:110xxxxx 代表它是雙字節 UTF-8 字符的首字節。
如果一個字節,以 10 開始,表示它不是首字節,需要向前查找才能得到當前字符的首字節
char c = '我';
但是事實並不是那么簡單,Java的char內部編碼為UTF-16,請參考String編碼(二) 證明JAVA的char編碼為UTF-16
Java 的char用兩字節存儲,表示范圍從 '\u0000' 到 '\uffff' ,也就是從0到65535。事實上,一個 char不能表示65535個字符,因為只有U+0000 到 U+D7FF 和 U+E000 到U+FFFF能用來表示 一個完整的字符,這些叫做 BMP(Basic Multilingual Plane),另外的作為high surrogate和 low surrogate 拼接組成由4字節表 示的字符。
在UTF-16編碼中,大於U+10000碼位將被編碼為一對16比特長的碼元,即按4個字節編碼,此時char無法表示。utf16編碼格式
所以Java的char只能表示utf16中的bmp部分字符。對於CJK(中日韓統一表意文字)部分擴展字符集則無法表示。
例如,下圖中除Ext-A部分,char均無法表示。
Unicode標准把代碼點分成了17個代碼平面(Code Plane),編號為#0到#16。每個代碼平面包含65,536(2^16)個代碼點(17*65,536=1,114,112)。其中,Plane#0叫做基本多語言平面(Basic Multilingual Plane,BMP),其余平面叫做補充平面(Supplementary Planes)。Unicode7.0只使用了17個平面中的6個,並且給這6個平面起了名字,如下圖所示:
下面是這些平面的名字和用途:
Plane#0 BMP(Basic Multilingual Plane)大部分常用的字符都坐落在這個平面內,比如ASCII字符,漢字等。
Plane#1 SMP(Supplementary Multilingual Plane)這個平面定義了一些古老的文字,不常用。
Plane#2 SIP(Supplementary Ideographic Plane)這個平面主要是一些BMP中沒有包含漢字。
Plane#14 SSP(Supplementary Special-purpose Plane)這個平面定義了一些非圖形字符。
Plane#15 SPUA-A(Supplementary Private Use Area A)
Plane#16 SPUA-B(Supplementary Private Use Area B)
UTF-16是Unicode字符集的一種轉換方式,即把Unicode的碼位轉換為16比特長的碼元串行,以用於數據存儲或傳遞。UTF-16編碼規則如下:
2.2.1 從U+D800到U+DFFF的碼位(代理區)
因為Unicode字符集的編碼值范圍為0-0x10FFFF,而大於等於0x10000的輔助平面區的編碼值無法用2個字節來表示,所以Unicode標准規定:基本多語言平面內,U+D800..U+DFFF的值不對應於任何字符,為代理區。因此,UTF-16利用保留下來的0xD800-0xDFFF區段的碼位來對輔助平面的字符的碼位進行編碼。
但是在使用UCS-2的時代,U+D800..U+DFFF內的值被占用,用於某些字符的映射。但只要不構成代理對,許多UTF-16編碼解碼還是能把這些不符合Unicode標准的字符映射正確的辨識、轉換成合規的碼元. 按照Unicode標准,這種碼元串行本來應算作編碼錯誤.
2.2.2 從U+0000至U+D7FF以及從U+E000至U+FFFF的碼位
第一個Unicode平面(BMP),碼位從U+0000至U+FFFF(除去代理區),包含了最常用的字符。UTF-16與UCS-2編碼在這個范圍內的碼位為單個16比特長的碼元,數值等價於對應的碼位。BMP中的這些碼位是僅有的碼位可以在UCS-2被表示。
2.2.3 從U+10000到U+10FFFF的碼位
輔助平面(Supplementary Planes)中的碼位,大於等於0x10000,在UTF-16中被編碼為一對16比特長的碼元(即32bit,4Bytes),稱作 code units called a 代理對(surrogate pair),具體方法是:
Ø 碼位減去0x10000, 得到的值的范圍為20比特長的0..0xFFFFF(因為Unicode的最大碼位是0x10ffff,減去0x10000后,得到的最大值是0xfffff,所以肯定可以用20個二進制位表示),寫成二進制形式:yyyy yyyy yyxx xxxx xxxx。
Ø 高位的10比特的值(值的范圍為0..0x3FF)被加上0xD800得到第一個碼元或稱作高位代理(high surrogate), 值的范圍是0xD800..0xDBFF。由於高位代理比低位代理的值要小,所以為了避免混淆使用,Unicode標准現在稱高位代理為前導代理(lead surrogates)。
Ø 低位的10比特的值(值的范圍也是0..0x3FF)被加上0xDC00得到第二個碼元或稱作低位代理(low surrogate), 現在值的范圍是0xDC00..0xDFFF。 由於低位代理比高位代理的值要大,所以為了避免混淆使用,Unicode標准現在稱低位代理為后尾代理(trail surrogates)。
Ø 最終的UTF-16(4字節)的編碼(二進制)就是:110110yyyyyyyyyy 110111xxxxxxxxxx。
按照上述規則,Unicode編碼0x10000-0x10FFFF的UTF-16編碼有兩個WORD,第一個WORD的高6位是110110,第二個WORD的高6位是110111。可見,第一個WORD的取值范圍(二進制)是11011000 00000000到11011011 11111111,即0xD800-0xDBFF。第二個WORD的取值范圍(二進制)是11011100 00000000到11011111 11111111,即0xDC00-0xDFFF。上面所說的從U+D800到U+DFFF的碼位(代理區),就是為了將一個WORD(2字節)的UTF-16編碼與兩個WORD的UTF-16編碼區分開來。
由於高位代理、低位代理、BMP中的有效字符的碼位,三者互不重疊,搜索是簡單的: 一個字符編碼的一部分不可能與另一個字符編碼的不同部分相重疊。這意味着UTF-16是自同步(self-synchronizing):可以通過僅檢查一個碼元就可以判定給定字符的下一個字符的起始碼元。 UTF-8也有類似優點,但許多早期的編碼模式就不是這樣,必須從頭開始分析文本才能確定不同字符的碼元的邊界。
由於最常有的字符都在基本多文種平面中,許多軟件的處理代理對的部分往往得不到充分的測試。這導致了一些長期的bug與潛在安全漏洞,甚至在廣為流行得到良好評價的應用軟件