1. 程式人生 > >UTF-8 ASCII GBK GB2312 GB18030等字元編碼的關係

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]=11100000(224) | b 0101 = b11100101 = 0xE5

var4[1]=10000000(128) | b 0101111100 & b 111111 = 10000000(128)b 111100b 10111100 = 0xBC (與運算大於或運算)

var4[2]=10000000(128) | b 01011111 00100000& b 111111 = 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擴充套件碼就可以了