字符、編碼和Java中的編碼

字符是用戶可以讀寫的最小單位。計算機所能支持的字符組成的集合,就叫做字符集。字符集通常以二維表的形式存在。二維表的內容和大小是由使用者的語言而定,是英語、是漢語、還是阿拉伯語。人類閱讀的文章是由字符組成的,而計算機是通過二進制字節進行信息傳輸的。計算機無法直接傳輸字符,所以就需要將字符解析成字節,這個解析操作就叫做編碼(encode),而相應的,將編碼的字節還原成字符的操作就叫做解碼(decode)。編碼和解碼都需要按照一定的規則,這種把字符集中的字符編碼為特定的二進制數的規則就是字符編碼(Character encoding)。網上也有稱之為字符集編碼的,其實大概就是一個意思,注意和后面的編碼字符集區分。

為什么要編碼

由于人類的語言有太多,因而表示這些語言的符號太多,無法用計算機中一個基本的存儲單元——字節 來表示,因而必須要經過拆分或一些翻譯工作,才能讓計算機能理解。計算機中存儲信息的最小單元是一個字節即8個位(bit),所以能表示的字符范圍是0~255個。人類要表示的符號太多,無法用一個字節來完全表示,這就需要編碼來解決這個問題。

早期字符編碼發展

在計算機發展的早期,字符集和字符編碼一般使用相同的命名,例如最早的字符集ASCII(American Standard Code for Information Interchange),它既代表了計算機所支持顯示的所有字符(字符集),又代表了這個字符集的字符編碼。ASCII字符集是一個二維表,支持128個字符。128個碼位,用7位二進制數表示,由于計算機1個字節是8位二進制數,所以最高位為0,即00000000-011111110x00-0x7F。ASCII(1963年)和EBCDIC(1964年)這樣的字符集逐漸成為計算機字符編碼的早期標準。但這些字符集的局限很快就變得明顯,于是人們開發了許多方法來擴展它們。

最初的拓展很簡單,只是在原來ASCII的基礎上,擴展為256個字符,成為EASCII(Extended ASCII)。EASCII有256個碼位,用8位二進制數表示,即00000000`11111111`或`0x00`0xFF

當計算機傳到了歐洲,EASCII也開始不能滿足需求了,但是改改還能湊合。于是國際標準化組織在ASCII的基礎上進行了擴展,形成了ISO-8859標準,跟EASCII類似,兼容ASCII,在高128個碼位上有所區別。但是由于歐洲的語言環境十分復雜,所以根據各地區的語言又形成了很多子標準,如ISO-8859-1、ISO-8859-2、ISO-8859-3、……、ISO-8859-16。

后來計算機傳入亞洲,由于亞洲語種和文字都十分豐富,256個碼位就顯得十分雞肋了。于是繼續擴大二維表,單字節改雙字節,16位二進制數,65536個碼位。在不同國家和地區又出現了很多編碼,大陸的GB2312、港臺的BIG5、日本的Shift JIS等等。

由于計算機的編碼規范和標準在最初制定時沒有意識到這將會是以后全球普適的準則,所以出現了各種各樣的編碼方式(也有了多種不同的字符集)。但是很多傳統的編碼方式都有一個共同的問題,即容許電腦處理雙語環境(通常使用拉丁字母以及其本地語言),但卻無法同時支持多語言環境(指可同時處理多種語言混合的情況)。而且不同國家和地區采用的字符集不一致,很可能出現無法正常顯示所有字符的情況。

所以對于支持包括東亞CJK字符家族在內的寫作系統的要求能支持更大量的字符,需要一種系統而不是臨時的方法實現這些字符的編碼。

Unicode字符編碼五層次模型

為了解決傳統的字符編碼方案的局限,就引入了統一碼(Unicode)和通用字符集(Universal Character Set, UCS)來替代原先基于語言的系統。通用字符集的目的是為了能夠涵蓋世界上所有的字符。由統一碼(Unicode)和通用字符集(Universal Character Set, UCS)所構成的現代字符編碼模型沒有跟從簡單字符集的觀點。它們將字符編碼的概念分為:有哪些字符、它們的編號、這些編號如何編碼成一系列的代碼單元,以及最后這些單元如何組成八位字節流。區分這些概念的核心思想是建立一個能夠用不同方法來編碼的一個通用字符集。

為了正確地表示Unicode和通用字符集模型,需要更多比“字符集”和“字符編碼”更為精確的術語表示。在Unicode Technical Report (UTR) #17中,現代編碼模型分為5個層次。

抽象字符表(Abstract character repertoire)

抽象字符表是操作系統支持的所有抽象字符的集合,決定了計算機能夠展現表示的所有字符的范圍。字符表中的字符可以決定如何劃分輸入的信息流。例如將一段中文劃分成文字、標點、空格等,它們都能按照一種簡單的線性序列排列。值得一提的是,對它們的處理需要另外的規則,如帶有變音符號的字母這樣的特定序列如何解釋。為了方便起見,這樣的字符表可以包括預先編號的字母和變音符號的組合。其它的書寫系統,如阿拉伯語和希伯萊語,由于要適應雙向文字和在不同情形下按照不同方式交叉在一起的字形,就使用更為復雜的符號表表示。

字符表可以是封閉的,即除非創建一個新的標準(ASCII和多數ISO/IEC 8859系列都是這樣的例子),否則不允許添加新的符號;字符表也可以是開放的,即允許添加新的符號(統一碼和一定程度上代碼頁是這方面的例子)。

編碼字符集(CCS:Coded Character Set)

編碼字符集是將字符集C中每個字符映射到1個坐標(整數值對:x, y)或者表示為1個非負整數NC中的每個字符對應的坐標或非負整數就稱為碼位(code position,也稱碼點:code point)。在ASCII中(這里的ASCII僅指編碼字符集)的字符A對應一個數字65,這個數字就是A的碼位;在GB 2312中字在45區82位,所以其碼位是45 82。碼位其實也就是編碼空間中的一個位置(position)。一個字符所占用的碼位稱為碼位值(code point value)。

由于GB 2312 用區和位來表示字符,因此也稱為區位碼

1個編碼字符集就是把抽象字符映射為碼位值,編碼字符集就是字符集和碼位的映射,也是一個元素為映射的集合。多個編碼字符集可以表示同樣的字符表,例如ISO-8859-1和IBM的代碼頁037和代碼頁500含蓋同樣的字符表但是將字符映射為不同的整數。這樣就產生了編碼空間(encoding space)的概念。

所謂的編碼空間,就是包含所有字符的表的維度。如果字符集的字符映射到一個非負整數N,如ISO-8859-1字符集中有256個字符,那就需要256個數字,每個字符對應一個數字,這所有的256個數字就構成了編碼空間 (Code space),即編碼空間為256(256個碼位)。如果字符集中的字符映射到一個坐標,那就用一對整數來描述,如GB 2312 的漢字編碼空間是94 x 94。

編碼空間也可以用字符的存儲單元尺寸來描述,例如:ISO-8859-1是一個8比特的編碼空間。編碼空間還可以用其子集來表述,如行、列、面(plane)等。

字符編碼表(CEF:Character Encoding Form)

字符編碼表也稱為"storage format",是將編碼字符集的碼位轉換成碼元(code units,也稱“代碼單元”)的序列。

碼元指一個已編碼的文本中具有最短的位(bit)組合的單元,是一個有限位長的整型值。對于UTF-8來說,碼元是8位長;對于UTF-16來說,碼元是16位長;對于UTF-32來說,碼元是32位長[1]。碼值(Code Value)是過時的用法。

定長編碼的字符編碼表是碼位到自身的映射(null mapping),但變長編碼中有些碼位只映射到一個碼元,而另一些碼位會映射到多個碼元(即由多個碼元組成的序列)。例如,使用16位長的存儲單元保存數字信息,系統每個單元只能夠直接表示從0到65,535的數值,但是如果使用多個16位單元就能夠表示更大的整數。這種映射可以使得在位長不變的情況下映射無限多的碼元,這就是CEF的作用。

最簡單的字符編碼表就是單純地選擇足夠大的單位,以保證編碼字符集中的所有數值能夠直接編碼(一個碼位對應一個碼值)。這對于能夠用使用八位元組來表示的編碼字符集(如多數傳統的非CJK的字符集編碼)是合理的,對于能夠使用十六位元來表示的編碼字符集(如早期版本的Unicode)來說也足夠合理。但是,隨著編碼字符集的大小增加(例如,現在的Unicode的字符集至少需要21位才能全部表示),這種直接表示法變得越來越沒有效率,并且很難讓現有計算機系統適應更大的碼值。因此,許多使用新近版本Unicode的系統,或者將Unicode碼位對應為可變長度的8位字節序列的UTF-8,或者將碼位對應為可變長度的16位序列的UTF-16。

字符編碼方案(CES:Character Encoding Scheme)

字符編碼方案也稱作"serialization format"。它將定長的整型值(即碼元)映射到8位字節序列,以便編碼后的數據的文件存儲或網絡傳輸。

Unicode僅使用一個簡單的字符來指定字節順序是大端序或者小端序(但對于UTF-8來說并不需要專門指明字節序)。有些復雜的字符編碼機制(如ISO/IEC 2022)會使用控制字符轉義序列在幾種編碼字符集或者壓縮機制之間切換。壓縮機制用于減小每個單元所用字節數,常見的壓縮機制有SCSU、BOCU和Punycode。

傳輸編碼語法(transfer encoding syntax)

傳輸編碼語法用于處理上一層次的字符編碼方案提供的字節序列。一般其功能包括兩種:一是把字節序列的值映射到一套更受限制的值域內,以滿足傳輸環境的限制,例如Email傳輸時Base64或者quoted-printable,都是把8位的字節編碼為7位長的數據;另一是壓縮字節序列的值,如LZW或者行程長度編碼等無損壓縮技術。

五層模型與傳統編碼的術語比較

歷史上的術語字符編碼(character encoding),字符映射(character map),字符集(character set)或者代碼頁往往是同義概念,即字符表(repertoire)中的字符如何編碼為碼元的流(stream of code units)。通常每個字符對應單個碼元。

所以例如ASCII這個名稱,可能表示抽象字符集(ACR)、編碼字符集(CCS)、字符編碼表(CEF)、字符編碼方案(CES)的任意一個或多個層次,在傳統編碼概念中ASCII這個名稱也本身就代表一種字符編碼,或者是一種字符集。具體表示什么要看上下文語義。平常我們所說的編碼都在第三步的時候完成了,都沒有涉及到CES。

代碼頁(Codepage)通常意味著面向字節的編碼,但強調是一套用于不能語言的編碼方案的集合。著名的如"Windows"代碼頁系列,"IBM"/"DOS"代碼頁系列。

字符映射(character map)在Unicode中保持了其傳統意義:從字符序列到編碼后的字節序列的映射,包括了上述的CCS, CEF, CES層次。

高層機制(higher level protocol)提供了額外信息,用于選擇Unicode字符的特定變種,如XML屬性xml:lang

由于不同國家和地區采用的字符集不一致,很可能出現無法正常顯示所有字符的情況。微軟公司使用了代碼頁轉換表的技術來過渡性的部分解決這一問題,即通過指定的轉換表將非Unicode的字符編碼轉換為同一字符對應的系統內部使用的Unicode編碼。

Unix或Linux不使用代碼頁概念,它們用charmap,比locales具有更廣泛的含義。

與上文的編碼字符集(Coded Character Set - CCS)不同,字符編碼(character encoding)是從抽象字符到代碼字(code word)的映射。

HTTP(與MIME)的用法中,字符集(character set)與字符編碼同義,但與CCS不是一個意思。

Unicode

Unicode(萬國碼、國際碼、統一碼、單一碼)是計算機科學領域里的一項業界標準。它并不是一種具體的字符編碼,而是對世界上大部分的文字系統進行了整理、編碼,使得電腦可以用更為簡單的方式來呈現和處理文字。

Unicode伴隨著通用字符集的標準而發展,至今仍在不斷增修,每個新版本都加入更多新的字符。Unicode編碼包含了不同寫法的字,如“ɑ/a”、“強/強”、“戶/戶/戸”。可以簡單地把通用字符集理解為五層模型中的抽象字符集(ACR),而Unicode就是字符編碼表(CCS)。—— Wikipadia: Unicode

編碼方式

Unicode使用16位的編碼空間也就是每個字符占用2個字節。這樣理論上一共最多可以表示2的16次方(即65536)個字符。基本滿足各種語言的使用。實際上當前版本的統一碼并未完全使用這16位編碼,而是保留了大量空間以作為特殊使用或將來擴展。

隨著Unicode的拓展,又增加了16個這樣的平面。一開始的平面就稱為基本多文種平面,也稱0號平面,剩余的叫做輔助平面。兩者合起來至少需要占據21位的編碼空間,比3字節略少。Unicode將0到140萬的編碼空間范圍的每個碼位映射到單個或多個在0到655356范圍內的碼元。

事實上輔助平面字符仍然占用4字節編碼空間,與UCS-4保持一致。未來版本會擴充到ISO 10646-1實現級別3,即涵蓋UCS-4的所有字符。UCS-4是一個更大的尚未填充完全的31位字符集,加上恒為0的首位,共需占據32位,即4字節。理論上最多能表示231個字符,完全可以涵蓋一切語言所用的符號。

更詳細請參考Unicode字符平面映射

實現方式

Unicode的實現方式不同于編碼方式。一個字符的Unicode編碼是確定的。但是在實際傳輸過程中,由于不同系統平臺的設計不一定一致,以及出于節省空間的目的,對Unicode編碼的實現方式有所不同。

Unicode的實現方式也稱為Unicode轉換格式(Unicode Transformation Format,簡稱為UTF),目前主流的實現方式有UTF-16和UTF-8。隨著Unicode通用字符集的擴充,進而出現了UTF-32,但是由于占用空間太大,目前很少有系統選擇使用utf-32作為系統編碼。

下面簡單介紹UTF-8和UTF-16的實現。

UTF-8

如果一個僅包含基本7位ASCII字符的Unicode文件,如果每個字符都使用2字節的原Unicode編碼傳輸,其第一字節的8位始終為0,這就造成了比較大的浪費。對于這種情況,可以使用UTF-8編碼,這是一種變長編碼,它將基本7位ASCII字符仍用7位編碼表示,占用一個字節(首位補0)。而遇到與其他Unicode字符混合的情況,將按一定算法轉換。

UTF-8每個字符使用1-3個字節編碼,并利用首位為0或1進行識別。

對于UTF-8編碼中的任意字節 B:

  • 如果B的第一位為0,那么代表當前字符為單字節字符,占用一個字節的空間。0之后的所有部分(7個位)代表在Unicode中的序號。
  • 如果B以110開頭,那么代表當前字符為雙字節字符,占用2個字節的空間。110之后的所有部分(5個位)加上后一個字節的除10外的部分(6個位)代表在Unicode中的序號。且第二個字節以10開頭
  • 如果B以1110開頭,那么代表當前字符為三字節字符,占用3個字節的空間。1110之后的所有部分(4個位)加上后兩個字節的除10外的部分(12個位)代表在Unicode中的序號。且第二、第三個字節以10開頭
  • 如果B以11110開頭,那么代表當前字符為四字節字符,占用4個字節的空間。11110之后的所有部分(3個位)加上后兩個字節的除10外的部分(18個位)代表在Unicode中的序號。且第二、第三、第四個字節以10開頭
  • 如果B以10開頭,則B為一個多字節字符中的其中一個字節(非ASCII字符)

如下表:

碼位的位數 碼位起值 字節序列 Byte1 Byte2 Byte3 Byte4 Byte5 Byte6
7 U+0000 U+007F 0xxx xxxx - - - - -
11 U+0080 U+07FF 110x xxxx 10xx xxxx - - - -
16 U+0800 U+FFFF 1110 xxxx 10xx xxxx 10xx xxxx - - -
21 U+10000 U+1FFFFF 1111 0xxx 10xx xxxx 10xx xxxx 10xx xxxx - -
26 U+200000 U+3FFFFFF 1111 10xx 10xx xxxx 10xx xxxx 10xx xxxx 10xx xxxx -
31 U+4000000 U+7FFFFFFF 1111 110x 10xx xxxx 10xx xxxx 10xx xxxx 10xx xxxx 10xx xxxx

基本多文種平面之外字符,使用4字節形式編碼。這些字符很多時候無法直接顯示在文本編輯器里。如??。有時候也會被用于區分錯誤的字符。

我們分別看三個從一個字節到三個字節的UTF-8編碼例子:

實際字符 在Unicode字庫序號的十六進制 在Unicode字庫序號的二進制 UTF-8編碼后的二進制 UTF-8編碼后的十六進制
$ 0024 010 0100 0010 0100 24
00A2 000 1010 0010 1100 0010 1010 0010 C2 A2
20AC 0010 0000 1010 1100 1110 0010 1000 0010 1010 1100 E2 82 AC

在文本編輯器中,一般會使用\u將一個十六進制數字轉換為Unicode字庫序號,進而識別出Unicode對應的字符。如$可以表示為\u0024
UTF-8的優缺點,以及其他更詳細的描述,可以參考Wikipedia: UTF-8

UTF-16

  • 如果字符編碼U小于0x10000,也就是十進制的0到65535之內,則直接使用兩字節表示;
  • 如果字符編碼U大于0x10000,由于UNICODE編碼范圍最大為0x10FFFF,從0x10000到0x10FFFF之間共有0xFFFFF個編碼,也就是需要20個bit就可以標示這些編碼。用U'表示從0-0xFFFFF之間的值,將其前 10 bit作為高位和16 bit的數值0xD800進行 邏輯or 操作,將后10 bit作為低位和0xDC00做 邏輯or 操作,這樣組成的 4個byte就構成了U的編碼。

由于一開始的Unicode只需要兩個字節,所以UTF-16雖然也是變長編碼方式,但是在最初卻可以當做定長編碼方式使用。UTF-16每個字符都直接使用兩個字節存儲,所以就有字節順序的問題,同一字節流可能會被解釋為不同內容。如某字符為十六進制編碼4E59,按兩個字節拆分為4E59,在Mac中和Windows中會解析如下:

- 讀取順序 顯示字符
Windows 4E 59
Mac 59 4E

在Mac上從低字節開始和在Windows上從高字節開始讀取顯示不同,從而導致在同一編碼下的亂碼問題。為了解決這個問題便引入了字節順序標記(英語:byte-order mark,BOM)來標記是大端序還是小端序。

對于輔助平面的字符,由于超過了一個16位可以表示的長度,所以需要兩個16位來表示。處于前面的16位被稱為前導,而后面的被稱為后綴。所以UTF-16要么是2字節,要么是4字節。

如何獲取前導和后綴?基本多文種平面有一段代理區,不代表任何字符,通過對代理區的計算,高10位加上0xD800就是前導,低10位加上0xDC00就是后綴。前導和后尾組成的代理對表示SP里的一個碼位。

很多人誤以為UTF-16在早期是定長編碼,其實它一開始就是變長的,同時期真正的二字節定長編碼是UCS-2。

BOM

字節順序標記(英語:byte-order mark,BOM)是一個有特殊含義的統一碼字符,碼點為U+FEFF。當以UTF-16或UTF-32來將UCS/統一碼字符所組成的字符串編碼時,這個字符被用來標示其字節序。經常被用于區分是否為UTF編碼。

字符U+FEFF如果出現在字節流的開頭,則用來標識該字節流的字節序,是高位在前還是低位在前。如果它出現在字節流的中間,則表達零寬度非換行空格的意義,用戶看起來就是一個空格。從Unicode3.2開始,U+FEFF只能出現在字節流的開頭,只能用于標識字節序,就如它的名稱——字節序標記——所表示的一樣;除此以外的用法已被舍棄。取而代之的是,使用U+2060來表達零寬度無斷空白。

UTF-8以字節為編碼單元,沒有字節序的問題。但是某些操作系統也會使用帶BOM的UTF-8,叫做UTF-8 with BOM。Python中叫utf-8-sig。Unicode規范中說明UTF-8不必也不推薦使用BOM。多數時候UTF-8都是不帶BOM的,但是微軟公司的某些軟件(如Excel)打開某些不帶BOM的utf8文件(如cvs文件)會亂碼,需要轉換成帶BOM的utf8編碼才能正常顯示。

所以Java中獲取以UTF-16編碼的字符串字節個數時,總是會比實際含有字符的字節個數多2。不過目前已經有很多主流的文本編輯器支持不帶BOM的UTF編碼了,通過后綴(LE和BE)區分是小端還是大端。

詳見Wikipedia: 字節順序標記

"Use of BOM is neither required nor recommended for UTF-8" ( Unicode 5.0.0 Chapter 2.6)
更詳細的區別請查閱知乎:「帶 BOM 的 UTF-8」和「無 BOM 的 UTF-8」有什么區別?

IDEA的UTF-8沒有BOM,但是如果打開(解碼)某個已經存在的文件,該文件使用帶BOM的UTF-8編碼,BOM會被忽略,但依然會保留。

另外,維基百科中闡述UTF-8和UTF-16屬于五層模型的字符編碼表(CEF)。但是個人理解,由于BOM的加入,實際上也包含了字符編碼方案(CES)一層。所以UTF-8和UTF-16的實現實際包含了CEF和CES兩個層次。UTF-8和UTF-16也可以說是基于Unicode的字符編碼。但和ASCII和GB 2312等字符編碼不同的是,前者使用通用字符集作為其ACR,而后者的ACR是自身規定(可能不通用)的。

知乎專欄: 字符編碼的那些事對字符編碼的闡述非常易懂,值得一看。

該使用什么編碼?

非Unicode編碼轉換不當會造成各種亂碼問題。那么具體應該如何選用合適的編碼?

存儲容量

先說UTF-16,由于每個碼位都使用2到4個字節來存儲,對于含有大量中文或者其他二字節長的字符流來說,UTF-16可以節省大量的存儲空間。因為UTF-16并不需要像UTF-8那樣通過犧牲很多標記位來標識一個字節表示的是什么,它只需一個字符來表示是大端序和小端序。

但是對于有大量西文字符的字符流來說UTF-8的優勢就變得十分明顯:UTF-8只需要一個字節就能存儲西文字符,這是UTF-16做不到的。所以在混合存儲,或者是源代碼、字節碼文件等大量西文字符的文件,更傾向于UTF-8。

UTF-8存儲中文比UTF-16要多出50%,不推薦要大量顯示中文的程序使用。—— 知乎輪子哥

而由于UTF-8的兼容性和對西文的支持,所以西方都提倡統一使用UTF-8作為字符編碼,這樣也的確可以徹底根除亂碼問題。目前基本上所有的開發環境和源代碼文件也基本上是統一UTF-8。

但是統一使用UTF-8真的就沒問題了嗎?不是的。

國內網站也曾經掀起過一陣子UTF-8的熱潮,小網站倒也沒什么,但幾個大型網站很快發現改用UTF-8之后流量費刷刷刷地往上漲,因為同樣一個漢字在GB2312里只有2字節,單在UTF-8里變成了3字節,流量增加50%,對于展示大量中文內容的網站來說簡直就是災難,即使使用所以過了沒多久大網站們紛紛打定主意堅守GB系編碼。對于需要數據庫需要存儲大量中文的網站,例如淘寶、CSDN等博客網站,不合適的編碼方式在流量峰值時會造成不小的流量開銷,必須是一個值得考慮的問題。

存儲效率

這里只從UTF-8和UTF-16兩個編碼來簡單闡述下效率問題。

因為每個字符使用不同數量的字節編碼,所以UTF-8編碼的字符串,尋找串中第N個字符是一個O(N)復雜度的操作。即串越長,則需要更多的時間來定位特定的字符。同時,還需要位變換來把字符編碼成字節,把字節解碼成字符。

而從UTF-16編碼規則來看,僅僅將字符的高位和地位進行拆分變成兩個字節。規則非常簡單,編碼效率很高,單字節O(1)的查找效率也非常好。

所以選什么編碼不是一句話、一篇文章可以決定的,必須通過一些流量測試和考量。目前大型的網站一般都不會只用一種單一的編碼,而是多種編碼混用,配合緩存和數據庫的表壓縮來減小流量壓力。

不過值得一提的是,這種時間效率問題正在隨著內存和CPU的發展而減小,現在已經不會作為主要考慮的問題了。

更多的討論參考知乎:編程語言的字符編碼選擇UTF-8和UTF-16的優缺點

Java中的編碼

由于Java高級語言的特性,如對字符串的封裝、char、運行時VM環境等對底層的多種封裝,Java的編碼也有很多值得詳談的地方。以上所有都是字符及編碼的基礎,下面來結合Java分析Java中的編碼。

Java外部的編碼

Java運行時環境和外部環境使用的編碼是不一樣的。外部環境的編碼可以使用Charset.defaultcharset()獲取。如果沒有指定外部環境編碼,就是操作系統的默認編碼。jvm操作I/O流時,如果不指定編碼,也會使用這個編碼,可以在啟動Java時使用-Dfile.encoding=xxx設置。通過System.setProperty("file.encoding","GBK")能修改這個值,但由于jvm一旦啟動就不能修改jvm默認字符集,所以修改這個值并沒有什么卵用。

file.encoding參數需要和sun.jnu.encoding作區分,后者主要設置的是下面三個地方的編碼:

  • 命令行參數
  • 主類名稱
  • 環境變量

關于Java外部使用的編碼,深入分析 Java 中的中文編碼問題已經說的非常詳細,所以本文在這里只講述JavaVM內部的編碼。

Java內部的編碼體系大致如圖:

Java編碼體系
Java編碼體系

可以看到Java運行時主要的兩個編碼就是UTF-8和UTF-16,而編譯的開始,就要將各種不同編碼的源代碼文件的轉碼成UTF-8。

其實不是UTF-8,是一種modified UTF-8,這里姑且先這么稱呼。

編譯時的編碼轉換

眾所周知,Java的源文件可以是任意的編碼,但是在編譯的時候,Javac編譯器默認會使用操作系統平臺的編碼解析字符,如果Java源文件是UTF-8編碼的話,會造成亂碼并拒絕編譯:

Javac拒絕編譯
Javac拒絕編譯

要想正確編譯,需要使用-encoding指定輸入的Java源碼文件的編碼:

-encoding encodingSet the source file encoding name, such as EUC-JP and UTF-8. If -encoding is not specified, the platform default converter is used.

Javac默認是使用操作系統平臺的編碼進行編譯,在簡體中文的Windows上,平臺默認編碼會是GBK,那么javac就會默認假定輸入的Java源碼文件是以GBK編碼的。

如果要想正確將源文件編譯,即從橙色的源碼文件到藍色的編譯器之間的箭頭上,需要一個“橋梁”,而這個“橋梁”就是這個-encoding參數。通過這個參數javac能夠正確讀取文件內容并將其中的字符串以UTF-8輸出到Class文件里,就跟自己寫個程序以GBK讀文件以UTF-8寫文件一樣。

當編譯期正確解碼源代碼字符后進行編譯操作,生成token和抽象語法樹,這時候就不再直接對源代碼文件的字符進行操作了,編譯器已經將其編碼為modified UTF-8的字節流并對字節流進行操作。導致亂碼的不是Java源碼編譯器的“編碼”(寫出UTF-8)的過程,而是“解碼”(讀入Java源碼內容)的過程。

JVM 規范中提到的modified UTF-8: Chapter 4. The class File FormatString content is encoded in modified UTF-8. Modified UTF-8 strings are encoded so that code point sequences that contain only non-null ASCII characters can be represented using only 1 byte per code point, but all code points in the Unicode codespace can be represented. Modified UTF-8 strings are not null-terminated. The encoding is as follows: ...

運行時數據中的UTF-16

JVM中運行時數據都是使用UTF-16進行編碼的。可能有個疑問,既然UTF-8兼容性那么好,為何不統一使用UTF-16,而使用UTF-8?

于是又要開始講歷史了。在Unicode最初誕生的時候,由于當時只有一個16位長的基本多文種平面,也就是只有0~65535的空間,兩個字節剛好夠用。所以UTF-16相比UTF-8來說也是有很多優勢的。當時很多比較主流的OS或者VM都是使用UTF-16作為默認字符編碼,例如Windows NT和Java VM的runtime data,這也解釋了為什么Java中char是兩個字節。

Windows中的Unicode實際上代表UTF-16 LE,Unicode并不是一種編碼方式。如果還不理解Unicode是什么,就把它當成一個協議,而UTF-XX是對協議的一種實現吧 ..

但是到了2001年,中國人大舉入侵ISO和Unicode委員會,用已經頒布的GB18030-2000為基礎,在Unicode 3.1標準中一口氣加入了42711個CJK擴展字符,整個Unicode字符集一下增大到94205個字符,2個字節放不下了,UTF-16原來是變長編碼的事也被人想起來了(中國人偷笑,GB系列從第一天起就是變長編碼)。從此UTF-16就變得很尷尬,它一來存儲空間利用率不高,二來又是個變長編碼無法直接訪問其中的碼元。但是完全放棄UTF-16成本太高,所以現在JVM的運行時數據依然是UTF16編碼的。

由于成本問題不能放棄UTF-16,但是UTF-8的兼容性和流行程度,又使得JVM必須做點什么來使得其內部數據不會被編碼方式影響,于是就有了這個modified UTF-8。

那么modified UTF-8究竟是什么?

modified UTF-8

在通常用法下,Java程序語言在通過InputStreamReader和OutputStreamWriter讀取和寫入串的時候支持標準UTF-8。在Java內部,以及Class文件里存儲的字符串是以一種叫modified UTF-8的格式存儲的。DataInput和DataOutput的實現類也使用這種稍作修改的UTF-8來編碼Unicode字符串,在使用中一般不會獲取到modified UTF-8編碼的字符串,但也有例外,例如readUTF方法。

String.getBytes("UTF-8") //拿到的字符串的標準UTF-8編碼的字節數組
new String(bytes, "UTF-8") //是使用標準UTF-8的字節流構造String.

modified UTF-8 大致和 UTF-8 編碼相同,但是有以下三個不同點:

  • 空字符(null character,U+0000)使用雙字節的0xc0 0x80,而不是單字節的0x00。
  • 僅使用1字節,2字節和3字節格式,而UTF-8支持更多的字節
  • 基本多文種平面之外的補充字符以代理對(surrogate pairs)的形式表示

使用雙字節空字符保證了在已編碼字符串中沒有嵌入空字節。因為C語言等語言程序中,單字節空字符是用來標志字符串結尾的。當已編碼字符串放到這樣的語言中處理,一個嵌入的空字符將把字符串一刀兩斷。

modified UTF-8在沒有超過前三個字節表示的時候,和UTF-8編碼方式一樣,但是超過以后會以代理對(surrogate pairs)的形式表示。至于為什么要以代理對形式表示。這個是因為JVM的默認編碼是UTF-16導致的。

一開始的Unicode只有一個可以用16位長完全表示的基本多文種平面,所以Java中的字符(char)為16位長,一個char可以存所有的字符。后來Unicode增加了很多輔助平面,兩個字節存不下那些字符,但是為了向后兼容Java不可能更改它的基本語法實現,于是對于超過U+FFFF的字符 (就是所謂的擴展字符)就需要用兩個16位長數據來表示,modified UTF-8由UTF-16格式的代理碼元來代替原先的Unicode碼元作為字符編碼表的碼元。每個代理碼單元由3個字節(就是一個modified UTF-8編碼出來的最大字節長)表示。所以在Java內部數據是統一使用modified UTF-8進行編碼的,這個編碼解碼出來的碼元是UTF-16編碼出來的2字節。JVM把UTF-16編碼出來的16位長的數據(2字節,操作系統用8位長的數據,即1字節)作為最小單位進行信息交換。這樣的話既不改變原來JVM中的編碼規則,又減少了很多擴展字符從UTF-8轉碼到UTF-16時的運算量,是不是很刺激?

modified UTF-8保證了一個已編碼字符串可以一次編為一個UTF-16碼,而不是一次一個Unicode碼位,使得所有的Unicode字符都能在Java上顯示。

不過也不是沒有缺點的,使用modified UTF-8進行解碼解出來的是UTF-16編碼編出來的數據,而UTF-16處理擴展字符需要兩個16位長表示。也就是說,要用兩個代理碼元共同表示一個Unicode碼位。原本使用UTF-8編碼只需要最多4個字節就能存儲一個Unicode碼位,使用modified UTF-8編碼后卻需要6個字節來存儲兩個代碼單元。

總結起來就是,modified UTF-8是對UTF-16的再編碼,modified UTF-8和UTF-8是兩種完全不同的編碼

所以JVM無需解碼UTF-16的數據,modified UTF-8代理碼元會處理這個映射關系。

文末

個人整理、理解,錯漏之處在所難免,還望指教。

參考文獻和引用:

  1. Wikipedia: 字符編碼
  2. Wikipedia: Unicode
  3. Oracle Javadoc: Interface DataInput
  4. Unicode 5.0.0
  5. The Java? Virtual Machine Specification - Java SE 7 Edition
  6. IDEA Support: byte order mark, BOM problem with utf-8 files
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。