Java開罐頭——字符編碼最全解析

收納進(jìn)此專輯:I/O流官方中文指南系列概述及索引

內(nèi)容參考: 知乎 @胖胖
深入分析 Java I/O 的工作機(jī)制,
深入分析 Java 中的中文編碼問(wèn)題,
字符編碼筆記:ASCII懒震,Unicode和UTF-8

作者: @youyuge
個(gè)人博客站點(diǎn): https://youyuge.cn

一个扰、字節(jié)與字符

首先明確“字節(jié)(Byte)”和“字符(Character)”的大械菡:

  • 1 byte = 8 bit
  • 1 char = 2 byte = 16 bit (Java默認(rèn)UTF-16編碼)

雖然1 bit才是數(shù)據(jù)真正的最小單位办龄,但1 bit 的信息量太少了俐填。要表示一個(gè)有用的信息英融,需要好幾個(gè)bit一起表示驶悟。所以除了硬件層面存在1個(gè)比特位的寄存器材失,大多數(shù)情況下龙巨,字節(jié)是數(shù)據(jù)最小的基本單位。我們熟知的基本型的大小都是8 bit(也就是1字節(jié))的整數(shù)倍:

  • boolean: 1 byte
  • short: 2 byte
  • int: 4 byte
  • float: 4 byte
  • long: 8 byte
  • double: 8 byte

二耘眨、為何要編碼

要回答這個(gè)問(wèn)題必須要回到計(jì)算機(jī)是如何表示我們?nèi)祟惸軌蚶斫獾姆?hào)的境肾,這些符號(hào)也就是我們?nèi)祟愂褂玫恼Z(yǔ)言奥喻。由于人類的語(yǔ)言有太多环鲤,因而表示這些語(yǔ)言的符號(hào)太多冷离,無(wú)法用計(jì)算機(jī)中一個(gè)基本的存儲(chǔ)單元—— byte 來(lái)表示,因而必須要經(jīng)過(guò)拆分或一些翻譯工作痹栖,才能讓計(jì)算機(jī)能理解瞭空。我們可以把計(jì)算機(jī)能夠理解的語(yǔ)言假定為英語(yǔ)咆畏,其它語(yǔ)言要能夠在計(jì)算機(jī)中使用必須經(jīng)過(guò)一次翻譯旧找,把它翻譯成英語(yǔ)钦讳。這個(gè)翻譯的過(guò)程就是編碼愿卒。所以可以想象只要不是說(shuō)英語(yǔ)的國(guó)家要能夠使用計(jì)算機(jī)就必須要經(jīng)過(guò)編碼琼开。這看起來(lái)有些霸道柜候,但是這就是現(xiàn)狀,這也和我們國(guó)家現(xiàn)在在大力推廣漢語(yǔ)一樣鹦肿,希望其它國(guó)家都會(huì)說(shuō)漢語(yǔ)箩溃,以后其它的語(yǔ)言都翻譯成漢語(yǔ)涣旨,我們可以把計(jì)算機(jī)中存儲(chǔ)信息的最小單位改成漢字霹陡,這樣我們就不存在編碼問(wèn)題了止状。

所以總的來(lái)說(shuō)导俘,編碼的原因可以總結(jié)為:

  1. 計(jì)算機(jī)中存儲(chǔ)信息的最小單元是一個(gè)字節(jié)即 8 個(gè) bit旅薄,所以能表示的字符范圍是 0~255 個(gè)
  2. 人類要表示的符號(hào)太多少梁,無(wú)法用一個(gè)字節(jié)來(lái)完全表示
  3. 要解決這個(gè)矛盾必須需要一個(gè)新的數(shù)據(jù)結(jié)構(gòu) char凯沪,從 char 到 byte 必須編碼
方向要搞清楚

三妨马、各類編碼規(guī)范

3.1 ASCII

我們知道烘跺,在計(jì)算機(jī)內(nèi)部滤淳,所有的信息最終都表示為一個(gè)二進(jìn)制的字符串。每一個(gè)二進(jìn)制位(bit)有0和1兩種狀態(tài)汇歹,因此八個(gè)二進(jìn)制位就可以組合出256種狀態(tài)产弹,這被稱為一個(gè)字節(jié)(byte)笔喉。也就是說(shuō)常挚,一個(gè)字節(jié)一共可以用來(lái)表示256種不同的狀態(tài)奄毡,每一個(gè)狀態(tài)對(duì)應(yīng)一個(gè)符號(hào)吼过,就是256個(gè)符號(hào),從0000000到11111111酱床。
上個(gè)世紀(jì)60年代扇谣,美國(guó)制定了一套字符編碼罐寨,對(duì)英語(yǔ)字符與二進(jìn)制位之間的關(guān)系鸯绿,做了統(tǒng)一規(guī)定瓶蝴。這被稱為ASCII碼租幕,一直沿用至今令蛉。
ASCII碼一共規(guī)定了128個(gè)字符的編碼蝎宇,比如空格"SPACE"是32(二進(jìn)制00100000)姥芥,大寫的字母A是65(二進(jìn)制01000001)汇鞭。這128個(gè)符號(hào)(包括32個(gè)不能打印出來(lái)的控制符號(hào))霍骄,只占用了一個(gè)字節(jié)的后面7位读整,最前面的1位統(tǒng)一規(guī)定為0米间。

ASCII

3.2 ISO-8859-1

但西方世界不光只有英語(yǔ)一門語(yǔ)言屈糊。什么德語(yǔ)逻锐,法語(yǔ)谦去,西班牙語(yǔ)都有自己的特殊字母鳄哭。但這也沒(méi)什么大不了的。每個(gè)國(guó)家都可以定義屬于自己語(yǔ)言的特殊編碼標(biāo)準(zhǔn)锄俄,而且大小照樣不超過(guò)256奶赠。因?yàn)锳SCII碼中本身就有很多空碼位沒(méi)有使用药有。

128 個(gè)字符顯然是不夠用的,于是 ISO 組織在 ASCII 碼基礎(chǔ)上又制定了一些列標(biāo)準(zhǔn)用來(lái)擴(kuò)展 ASCII 編碼赘理,它們是 ISO-8859-1~ISO-8859-15商模,其中 ISO-8859-1 涵蓋了大多數(shù)西歐語(yǔ)言字符蜘澜,所有應(yīng)用的最廣泛鄙信。ISO-8859-1 仍然是單字節(jié)編碼,它總共能表示 256 個(gè)字符趟章。

ISO-8859-1

3.3 GB2312

它的全稱是《信息交換用漢字編碼字符集 基本集》蚓土,它是雙字節(jié)編碼蜀漆,總的編碼范圍是 A1-F7确丢,其中從 A1-A9 是符號(hào)區(qū)吐限,總共包含 682 個(gè)符號(hào)诸典,從 B0-F7 是漢字區(qū),包含 6763 個(gè)漢字舀寓。但是還是遠(yuǎn)遠(yuǎn)不夠不是嗎互墓?

3.4 GBK

全稱叫《漢字內(nèi)碼擴(kuò)展規(guī)范》篡撵,是國(guó)家技術(shù)監(jiān)督局為 windows95 所制定的新的漢字內(nèi)碼規(guī)范,它的出現(xiàn)是為了擴(kuò)展 GB2312骂租,加入更多的漢字斑司,它的編碼范圍是 8140~FEFE(去掉 XX7F)總共有 23940 個(gè)碼位宿刮,它能表示 21003 個(gè)漢字僵缺,它的編碼是和 GB2312 兼容的磕潮,也就是說(shuō)用 GB2312 編碼的漢字可以用 GBK 來(lái)解碼自脯,并且不會(huì)有亂碼膏潮。

這也是很多文件默認(rèn)使用的編碼满力,有時(shí)候打開文件中文變亂碼了油额,這時(shí)候就需要規(guī)定編碼方式為GBK潦嘶。(比如eclipse打開別人的java文件衬以,中文注釋亂碼了)

3.5 Unicode

需要注意的是饱亿,Unicode只是一個(gè)符號(hào)集笔刹,它只規(guī)定了符號(hào)的二進(jìn)制代碼坤塞,卻沒(méi)有規(guī)定這個(gè)二進(jìn)制代碼應(yīng)該如何存儲(chǔ)澈蚌。

  • 在出現(xiàn)Unicode之前宛瞄,幾乎每一種文字都有一套自己的編碼方式。同一段“字節(jié)流”盈电,在美帝可能是"hello world"匆帚,到我們天朝就變成“錕斤拷” 旁钧,“燙燙燙”了歪今。

  • ISO 試圖想創(chuàng)建一個(gè)全新的超語(yǔ)言字典鞭铆,世界上所有的語(yǔ)言都可以通過(guò)這本字典來(lái)相互翻譯焦影∷钩剑可想而知這個(gè)字典是多么的復(fù)雜彬呻,關(guān)于 Unicode 的詳細(xì)規(guī)范可以參考相應(yīng)文檔。

  • 最初剪况,每個(gè)字符占用2個(gè)字節(jié),總共65535個(gè)字符空間或悲。從第四版開始加入的“擴(kuò)展字符集”開始使用4個(gè)字節(jié)(32 bit)編碼。目前Unicode收錄的字符規(guī)模大概在12萬(wàn)左右翎蹈。

比如合陵,把“尤”這個(gè)字符轉(zhuǎn)換成Unicode編碼則為:

3.6 UTF-16

編碼里最容易搞混的一件事就是:Unicode只是一套符號(hào)的編碼曙寡。但計(jì)算機(jī)具體怎么讀取這套編碼,又是另外一件事揩抡。

UTF-16 具體定義了 Unicode 字符在計(jì)算機(jī)中存取方法峦嗤。UTF-16 用兩個(gè)字節(jié)來(lái)表示 Unicode 轉(zhuǎn)化格式烁设,這個(gè)是定長(zhǎng)的表示方法,不論什么字符都可以用兩個(gè)字節(jié)表示装黑,兩個(gè)字節(jié)是 16 個(gè) bit,所以叫 UTF-16挽鞠。UTF-16 表示字符非常方便信认,每?jī)蓚€(gè)字節(jié)表示一個(gè)字符其掂,這個(gè)在字符串操作時(shí)就大大簡(jiǎn)化了操作潦蝇,這也是 Java 以 UTF-16 作為內(nèi)存的字符存儲(chǔ)格式的一個(gè)很重要的原因。

字符串“I am 君山”用 UTF-16 編碼翩迈,下面是編碼結(jié)果:

注意:因?yàn)镴ava中char默認(rèn)就是UTF-16編碼负饲,所以下面的char[]字符串是兩個(gè)字節(jié)表示一個(gè)字符返十。

“I am 君山”用 UTF-16 編碼

用 UTF-16 編碼將 char 數(shù)組放大了一倍洞坑,單字節(jié)范圍內(nèi)的字符迟杂,在高位補(bǔ) 0 變成兩個(gè)字節(jié),中文字符也變成兩個(gè)字節(jié)本慕。從 UTF-16 編碼規(guī)則來(lái)看排拷,僅僅將字符的高位和地位進(jìn)行拆分變成兩個(gè)字節(jié)。特點(diǎn)是編碼效率非常高锅尘,規(guī)則很簡(jiǎn)單监氢,但是這對(duì)于存儲(chǔ)來(lái)說(shuō)是極大的浪費(fèi),可以看到補(bǔ)了很多的0藤违,文本文件的大小會(huì)因此大出二三倍浪腐,這是無(wú)法接受的。

3.7 UTF-8

互聯(lián)網(wǎng)的普及顿乒,強(qiáng)烈要求出現(xiàn)一種統(tǒng)一的編碼方式牛欢。UTF-8就是在互聯(lián)網(wǎng)上使用最廣的一種Unicode的實(shí)現(xiàn)方式犹菱。其他實(shí)現(xiàn)方式還包括UTF-16(字符用兩個(gè)字節(jié)或四個(gè)字節(jié)表示)和UTF-32(字符用四個(gè)字節(jié)表示)访得,不過(guò)在互聯(lián)網(wǎng)上基本不用。

重復(fù)一遍,這里的關(guān)系是谈竿,UTF-8(或者UTF-16)是Unicode的實(shí)現(xiàn)方式之一呀洲。

UTF-8最大的一個(gè)特點(diǎn)族壳,就是它是一種變長(zhǎng)的編碼方式坏平。它可以使用1~4個(gè)字節(jié)表示一個(gè)符號(hào),根據(jù)不同的符號(hào)而變化字節(jié)長(zhǎng)度。
UTF-8的編碼規(guī)則很簡(jiǎn)單,只有二條:

  1. 對(duì)于單字節(jié)的符號(hào),字節(jié)的第一位設(shè)為0彬坏,后面7位為這個(gè)符號(hào)的unicode碼。因此對(duì)于英語(yǔ)字母,UTF-8編碼和ASCII碼是相同的。

  2. 對(duì)于n字節(jié)的符號(hào)(n>1)乏德,第一個(gè)字節(jié)的前n位都設(shè)為1郑什,第n+1位設(shè)為0兜粘,后面字節(jié)的前兩位一律設(shè)為10。剩下的沒(méi)有提及的二進(jìn)制位,全部為這個(gè)符號(hào)的unicode碼。

UTF-8可變長(zhǎng)編碼

我們?cè)賮?lái)看看字符串“I am 君山”用 UTF-8 編碼:

“I am 君山”用 UTF-8 編碼

可以看到?jīng)]有補(bǔ)0,沒(méi)有空間浪費(fèi)捎拯,相對(duì)的建芙,中文字用了3個(gè)字節(jié)存儲(chǔ)赶熟。“君”字UTF-8編碼成e5909b具體的編碼過(guò)程為:

“君”字UTF-8編碼

UTF-8 編碼代碼片段:

private CoderResult encodeArrayLoop(CharBuffer src, 
ByteBuffer dst){ 
           char[] sa = src.array(); 
           int sp = src.arrayOffset() + src.position(); 
           int sl = src.arrayOffset() + src.limit(); 
           byte[] da = dst.array(); 
           int dp = dst.arrayOffset() + dst.position(); 
           int dl = dst.arrayOffset() + dst.limit(); 
           int dlASCII = dp + Math.min(sl - sp, dl - dp); 
           // ASCII only loop 
           while (dp < dlASCII && sa[sp] < '\u0080') 
               da[dp++] = (byte) sa[sp++]; 
           while (sp < sl) { 
               char c = sa[sp]; 
               if (c < 0x80) { 
                   // Have at most seven bits 
                   if (dp >= dl) 
                       return overflow(src, sp, dst, dp); 
                   da[dp++] = (byte)c; 
               } else if (c < 0x800) { 
                   // 2 bytes, 11 bits 
                   if (dl - dp < 2) 
                       return overflow(src, sp, dst, dp); 
                   da[dp++] = (byte)(0xc0 | (c >> 6)); 
                   da[dp++] = (byte)(0x80 | (c & 0x3f)); 
               } else if (Character.isSurrogate(c)) { 
                   // Have a surrogate pair 
                   if (sgp == null) 
                       sgp = new Surrogate.Parser(); 
                   int uc = sgp.parse(c, sa, sp, sl); 
                   if (uc < 0) { 
                       updatePositions(src, sp, dst, dp); 
                       return sgp.error(); 
                   } 
                   if (dl - dp < 4) 
                       return overflow(src, sp, dst, dp); 
                   da[dp++] = (byte)(0xf0 | ((uc >> 18))); 
                   da[dp++] = (byte)(0x80 | ((uc >> 12) & 0x3f)); 
                   da[dp++] = (byte)(0x80 | ((uc >>  6) & 0x3f)); 
                   da[dp++] = (byte)(0x80 | (uc & 0x3f)); 
                   sp++;  // 2 chars 
               } else { 
                   // 3 bytes, 16 bits 
                   if (dl - dp < 3) 
                       return overflow(src, sp, dst, dp); 
                   da[dp++] = (byte)(0xe0 | ((c >> 12))); 
                   da[dp++] = (byte)(0x80 | ((c >>  6) & 0x3f)); 
                   da[dp++] = (byte)(0x80 | (c & 0x3f)); 
               } 
               sp++; 
           } 
           updatePositions(src, sp, dst, dp); 
           return CoderResult.UNDERFLOW; 
}

3.8 ANSI

ANSI是默認(rèn)的編碼方式浴骂。對(duì)于英文文件是ASCII編碼狡相,對(duì)于簡(jiǎn)體中文文件是GB2312編碼(只針對(duì)Windows簡(jiǎn)體中文版,如果是繁體中文版會(huì)采用Big5碼)曹宴。

四、幾種編碼格式的比較

  • 對(duì)中文字符后面四種編碼格式都能處理厅缺,GB2312 與 GBK 編碼規(guī)則類似,但是 GBK 范圍更大翻伺,它能處理所有漢字字符,所以 GB2312 與 GBK 比較應(yīng)該選擇 GBK。

  • UTF-16 與 UTF-8 都是處理 Unicode 編碼,它們的編碼規(guī)則不太相同蛋逾,相對(duì)來(lái)說(shuō) UTF-16 編碼效率最高,字符到字節(jié)相互轉(zhuǎn)換更簡(jiǎn)單彻坛,進(jìn)行字符串操作也更好。它適合在本地磁盤和內(nèi)存之間使用疙渣,可以進(jìn)行字符和字節(jié)之間快速切換谣沸,如 Java 的內(nèi)存編碼就是采用 UTF-16 編碼。但是它不適合在網(wǎng)絡(luò)之間傳輸航背,因?yàn)榫W(wǎng)絡(luò)傳輸容易損壞字節(jié)流障贸,一旦字節(jié)流損壞將很難恢復(fù)锋叨,想比較而言 UTF-8 更適合網(wǎng)絡(luò)傳輸听诸,對(duì) ASCII 字符采用單字節(jié)存儲(chǔ),另外單個(gè)字符損壞也不會(huì)影響后面其它字符动知,在編碼效率上介于 GBK 和 UTF-16 之間摊崭,所以 UTF-8 在編碼效率上和編碼安全性上做了平衡,是理想的中文編碼方式窘问。

五惠赫、總結(jié)

  • 總而言之,一切都是字節(jié)流,其實(shí)沒(méi)有字符流這個(gè)東西。字符只是根據(jù)編碼集對(duì)字節(jié)流翻譯之后的產(chǎn)物嫉柴。而編碼集是人為規(guī)定的產(chǎn)物。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末酬屉,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子惨远,更是在濱河造成了極大的恐慌谜悟,老刑警劉巖话肖,帶你破解...
    沈念sama閱讀 217,185評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異葡幸,居然都是意外死亡最筒,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門蔚叨,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)床蜘,“玉大人,你說(shuō)我怎么就攤上這事蔑水⌒暇猓” “怎么了?”我有些...
    開封第一講書人閱讀 163,524評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵搀别,是天一觀的道長(zhǎng)丹擎。 經(jīng)常有香客問(wèn)我,道長(zhǎng)歇父,這世上最難降的妖魔是什么鸥鹉? 我笑而不...
    開封第一講書人閱讀 58,339評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮庶骄,結(jié)果婚禮上毁渗,老公的妹妹穿的比我還像新娘。我一直安慰自己单刁,他們只是感情好灸异,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,387評(píng)論 6 391
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著羔飞,像睡著了一般肺樟。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上逻淌,一...
    開封第一講書人閱讀 51,287評(píng)論 1 301
  • 那天么伯,我揣著相機(jī)與錄音,去河邊找鬼卡儒。 笑死田柔,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的骨望。 我是一名探鬼主播硬爆,決...
    沈念sama閱讀 40,130評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼擎鸠!你這毒婦竟也來(lái)了缀磕?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,985評(píng)論 0 275
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎袜蚕,沒(méi)想到半個(gè)月后糟把,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,420評(píng)論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡牲剃,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,617評(píng)論 3 334
  • 正文 我和宋清朗相戀三年遣疯,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片颠黎。...
    茶點(diǎn)故事閱讀 39,779評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡另锋,死狀恐怖滞项,靈堂內(nèi)的尸體忽然破棺而出狭归,到底是詐尸還是另有隱情,我是刑警寧澤文判,帶...
    沈念sama閱讀 35,477評(píng)論 5 345
  • 正文 年R本政府宣布过椎,位于F島的核電站,受9級(jí)特大地震影響戏仓,放射性物質(zhì)發(fā)生泄漏疚宇。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,088評(píng)論 3 328
  • 文/蒙蒙 一赏殃、第九天 我趴在偏房一處隱蔽的房頂上張望敷待。 院中可真熱鬧,春花似錦仁热、人聲如沸榜揖。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,716評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)举哟。三九已至,卻和暖如春迅矛,著一層夾襖步出監(jiān)牢的瞬間妨猩,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,857評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工秽褒, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留壶硅,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,876評(píng)論 2 370
  • 正文 我出身青樓销斟,卻偏偏與公主長(zhǎng)得像森瘪,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子票堵,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,700評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容