UTF-8 ASCII GBK GB2312 GB18030等字元編碼的關係
準備寫一個 JNI HelloWorld:
public class HelloWorld {
public native void displayHelloWorld();//所有native關鍵詞修飾的都是對本地的宣告
static {
System.loadLibrary("hello");//載入本地庫
}
public static void main(String[] args) {
new HelloWorld().displayHelloWorld();
}
}
沒想到Javac HelloWorld.java 竟然報錯:
J:\MAKER\Java原始碼\JNI嘗試>javac Helloworld.java Helloworld.java:2: error: unmappable character for encoding GBK public native void displayHelloWorld();//鎵?鏈塶ative鍏抽敭璇嶄慨楗扮殑閮芥槸瀵規湰鍦扮殑澹版槑 ^ Helloworld.java:4: error: unmappable character for encoding GBK System.loadLibrary("hello");//杞藉靉鏈湴搴? ^ Helloworld.java:1: error: class, interface, or enum expected 鍩縫ublic class HelloWorld { ^ 3 errors
HelloWorld.java是以UTF-8格式編碼的,windows在中國用的是GBK編碼,(用Windows建立一個文字文件,其預設字符集就是GBK),因此出現的編碼錯誤
然後又順便去看了下 unicod、utf-8、ASCII 編碼的相關知識:
早期是ASCII(American Standard Code for Information Interchange,美國資訊交換標準程式碼)編碼,一個位元組表示英文和其他符號,後來計算機推廣後不夠用了。
隨著計算機的流行,其他語言也需要在計算機中表示,這時候ASCII開始拓展,出現IS0-8859-1簡“Latin 1”、DBCS(double-byte character set)等字元,只不過DBCS不再是國際推行標準,弄得字元表示非常混亂
後來出現了大一統的Unicode編碼(萬國碼,統一碼,1990年頒佈),Java裡的字元記憶體記錄的就是Unicode編碼,每個字元4個位元組(32bit)表示(相當於一張超級大表),比如漢字“張”(Unicode碼為‘0x5f20‘(b0101111100100000)),正規應該是‘0x00005f20’(b00000000000000000101111100100000),這樣空間太浪費了,出現了UTF-8編碼,規則如下
1)對於單位元組的符號,位元組的第一位設為0,後面7位為這個符號的 Unicode 碼。因此對於英語字母,UTF-8 編碼和 ASCII 碼是相同的。
2)對於n位元組的符號(n > 1),第一個位元組的前n位都設為1,第n + 1位設為0,後面位元組的前兩位一律設為10。剩下的沒有提及的二進位制位,全部為這個符號的 Unicode 碼。
下表總結了編碼規則,字母x表示可用編碼的位。
再以“張”為例:0x5F20(b01011111 00100000)在0x0000800-0x000FFFF之間,因此UTF-8格式為b1110XXXX 10XXXXXX 10XXXXXX(三位元組表示),因此“張” UTF-8 編碼為 :b11100101 10111100 10100000(0xE5BCA0)
那麼GBK又是怎麼一回事?
維基百科:
漢字內碼擴充套件規範,稱GBK,全名為《漢字內碼擴充套件規範(GBK)》1.0版,由中華人民共和國全國資訊科技標準化技術委員會1995年12月1日製訂,國家技術監督局標準化司和電子工業部科技與質量監督司1995年12月15日聯合以《技術標函[1995]229號》檔案的形式公佈。 GBK共收錄21886個漢字和圖形符號,其中漢字(包括部首和構件)21003個,圖形符號883個。
GBK的K為漢語拼音Kuo Zhan(擴充套件)中“擴”字的聲母。英文全稱Chinese Internal Code Extension Specification。
GBK 只為“技術規範指導性檔案”,不屬於國家標準。國家質量技術監督局於2000年3月17日推出了GB 18030-2000標準,以取代GBK。GB 18030-2000除保留全部GBK編碼漢字,在第二位元組把能使用範圍再度進行擴充套件,增加了大約一百個漢字及四位元組編碼空間,但是將GBK作為子集全部保留。
也就是說,GBK是中國人自己的編碼方式,剛開始只是一個技術規範,2000年推出的GB18030才是官方正式的標準且包含最初的GBK。現行版本為國家質量監督檢驗總局和中國國家標準化管理委員會於2005年11月8日釋出,2006年5月1日實施;
GB18030與Unicode的轉化比較複雜:
GB18030包括單位元組的ASCII、雙位元組的GBK(略帶擴充套件)、以及用於填補所有Unicode碼位的四位元組UTF區塊
那麼'a'與'張'這兩個字元是如何在GB18030中記錄的呢?
其和unicode一樣,也是一張大表
如:'a' -->GB18030--> 0x61; 'a' -->unicode--> 0x61 ,英文是不變的,這個像UTF-8實現unicode的方式。
' 張' -->GB18030--> 0xD5C5; '張' -->unicode--> 0x5F20,中文是不對應的。
因此以GBK方式編碼的字元,如果不通過中間介unicode轉化,則會出現亂碼。
而若想實現GBK與unicode的轉化,則必須有張類似於0xD5C5--> 0x5F20的對應(這個對應關係是沒有總體規律的)的位置偏移表,然後去找下Java的位置偏移表在哪裡,
Java萬物皆物件,封裝在Charset中,下面是尋找過程:
encode方法如下:
然後去查詢Charset,即lookupCharset方法,而後呼叫的是Charset中lookup方法:
且這裡能看到,java模式的字元編碼是UTF-8
這裡尋找的是GBK,發現有一個快取cache1很有意思,
網上總說Java效能差,這裡快取兩個JVM呼叫過的字符集以增強效能。
cache1[0]存的是字串型別,cache1[1]存的是Charset型別,所以lookup程式碼是查詢到了直接強轉返回(Charset)a[1] ,如果沒查詢到,呼叫lookup2方法去檢視cache2物件是否有:
有的話,需要維護一下快取,沒的話繼續查詢:
cs是我們的返回的目標Charset,他呼叫了standardProvider的charsetForName方法
standardProvider例項化物件是 StandardCharsets物件,這個類的原始碼裡存了很多編碼型別
然而並沒有GBK。
StandardCharsets繼承 FastCharsetProvider類,將上述的字元都存入以下兩個map裡了
回到charsetForName方法:
canonicalize(規範化的意思), 也就是你傳值utf8可以,也可以傳utf-8,他都給你返回utf-8
然後看lookup方法
這裡他又自己寫了轉化小寫方法,為了效能也是拼了。
lookup方法:
1、先從快取中取var3
2、1不是再從標準庫中取var4,這裡沒取到,剛才已經介紹過了。GBK不是國際標準。
3、2不是再看看是不是US_ASCII碼(也就是最原始的ASCII碼),var7
4、3不是的話就利用反射生成即sun.nio.cs.*生成相應的Charset返回。
sun.nio.cs中只要這麼些個類
這裡我們繼續回到Charset的lookup2方法,
然後在擴充套件字符集中找,
例項的是sun.nio.cs.ext.ExtendedCharsets物件,檢視其原始碼:
它也和StandardCharset一樣,有很多字符集,我查詢一下GBK:
這裡然後是呼叫父類方法查詢,找到後返回GBK的Charset,然後記得快取到cache1中。
然後再看StringCoding類中encode方法:
例項化字串編碼器:
而後主要是這段程式碼:
然後進入ArrayEncoder.encode方法:
var9是中文字元'張'的hashCode值。hashCode值也正好是unicode編碼:24352(b101111100100000)(0x5F20)
然後進行一個目前不知道為什麼的神祕操作:
24352(b101111100100000)>>8 -->(b1011111)--->95--->this.c2bIndex[95]=4864
24352&255--->(b101111100100000)
&(b000000011111111)-->b100000-->32
這個c2b的char型別陣列竟然有28662個字元,
32+4864=4896 this.c2b[4896]=
即var9=54725 (b1101010111000101),暫時不知道有什麼用,
var9>>8=b11010101 (前8位),強制轉化為byte只保留8位,然後最高位為符號位,補碼(11010101)表示為-43
var9強轉為byte只保留後8位即:b11000101,補碼(11000101)表示為-59
故var4是一個byte型別陣列,兩兩一組,這裡var4[0]為'張'前8位資訊,var4[1]為'張'後8位資訊,
而b1101010111000101 轉化為16進位制就是0xD5C5,即GBK中對'張'的編碼
這裡的c2b和c2bIndex便是位置偏移表,那麼在哪個檔案中呢?
在sun.nio.cs.ext.GBK中,DoubleBytes呼叫initC2B方法,初始化得到位置偏移表。
那麼UTF-8在Java中的編碼是什麼過程,沒有位移表了吧?
是的,此時是用sun.nio.cs.UT_8類中的encode方法
就是按照上述的兩條規則而寫的函式, '張'字走的核心程式碼為:
以“張”為例:0x5F20(b01011111 00100000)在0x0000800-0x000FFFF之間,因此UTF-8格式為b1110XXXX 10XXXXXX 10XXXXXX(三位元組表示),因此“張” UTF-8 編碼為 :b11100101 10111100 10100000(0xE5BCA0)
這裡var8即(b01011111 00100000),var4目標陣列,var6是下標,此時為0,則
var4[0]=b 11100000(224) | b 0101 = b11100101 = 0xE5
var4[1]=b 10000000(128) | b 0101111100 & b 111111 = b 10000000(128)| b 111100= b 10111100 = 0xBC (與運算大於或運算)
var4[2]=b 10000000(128) | b 01011111 00100000& b 111111 = b 10000000(128)| b 100000 = b 10100000 = 0xA0 (與運算大於或運算)
此時張的UTF-8編碼就是0xE5BCA0
但我還發現:ASCII碼也能編碼中文了,這是因為前文所介紹的ASCII擴充套件編碼DBCS。那麼漢字使用ASCII又是如何表示的呢?
在最初的ASCII中,後7位都用英文表示了,留下了高位沒用,此時有計算機的各國開始用剩下的127個碼位,中國用,日本用,其他國也用,非常混亂,DBCS是高位的127個碼位也不夠用了,就使用雙位元組來表示:
規定:一個小於127的字元的意義與原來相同,但兩個大於127的字元連在一起時,就表示一個漢字,前面的一個位元組(他稱之為高位元組)從0xA1用到 0xF7,後面一個位元組(低位元組)從0xA1到0xFE,這樣我們就可以組合出大約7000多個簡體漢字了。在這些編碼裡,我們還把數學符號、羅馬希臘的字母、日文的假名們都編進去了,連在 ASCII 裡本來就有的數字、標點、字母都統統重新編了兩個位元組長的編碼,這就是常說的"全形"字元,而原來在127號以下的那些就叫"半形"字元了
大家覺得這很不錯,於是就稱其為GB2312 ,GB2312 是對 ASCII 的中文擴充套件。 後來發展為GBK,最後國家出臺了標準GB18030。
那麼既然有大一統的unicode編碼,為什麼還要有GBK呢?
這得看下時間:從GB2312(1980年)、GBK(1995年)到GB18030(2000年),這些編碼方法是向下相容的
而unicode 1990年開始研發,1994年正式公佈。顯然滿足不了我們的需求。
最後。
UTF-8 可變長的, 1-4個位元組 1-32位,但漢字是3個位元組。
GB2312和GBK都是DBCS。 2個位元組,1-16位
GB18030,也是可變的。
下面是證明
1、Java中對“張a”的輸出:
===============GBK2310=========================
d5
c5
61
===============GBK231=========================
d5
c5
61
===============GBK18030=========================
d5
c5
61
===============utf8=========================
e5
bc
a0
61
2、UltraEdit 中的"十六進位制功能":
windows中新建一個txt輸入,張a,用UltraEdit的十六進位制檢視就是:
因此此時編碼是 ASCII拓展編碼 GBK
但若以UTF-8編碼‘張a張’:
能看到E5BCA0,但前面的EFBBBF是什麼呢?
Little endian 和 Big endian
上一節已經提到,UCS-2 格式可以儲存 Unicode 碼(碼點不超過0xFFFF)。以漢字嚴為例,Unicode 碼是4E25,需要用兩個位元組儲存,一個位元組是4E,另一個位元組是25。儲存的時候,4E在前,25在後,這就是 Big endian 方式;25在前,4E在後,這是 Little endian 方式。這兩個古怪的名稱來自英國作家斯威夫特的《格列佛遊記》。在該書中,小人國裡爆發了內戰,戰爭起因是人們爭論,吃雞蛋時究竟是從大頭(Big-endian)敲開還是從小頭(Little-endian)敲開。為了這件事情,前後爆發了六次戰爭,一個皇帝送了命,另一個皇帝丟了王位。
第一個位元組在前,就是"大頭方式"(Big endian),第二個位元組在前就是"小頭方式"(Little endian)。
那麼很自然的,就會出現一個問題:計算機怎麼知道某一個檔案到底採用哪一種方式編碼?
Unicode 規範定義,每一個檔案的最前面分別加入一個表示編碼順序的字元,這個字元的名字叫做"零寬度非換行空格"(zero width no-break space),用FEFF表示。這正好是兩個位元組,而且FF比FE大1。
如果一個文字檔案的頭兩個位元組是FE FF,就表示該檔案採用大頭方式;如果頭兩個位元組是FF FE,就表示該檔案採用小頭方式。
這裡的EFBBBF表示UTF8編碼方式,
而unicode是:
littleEndian 張a張
Big endian 張a張
因此java中預設是big endian。
因此剛開始的問題可以解決了,轉化為ASCII擴充套件碼就可以了