收納進(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é)為:
- 計(jì)算機(jī)中存儲(chǔ)信息的最小單元是一個(gè)字節(jié)即 8 個(gè) bit旅薄,所以能表示的字符范圍是 0~255 個(gè)
- 人類要表示的符號(hào)太多少梁,無(wú)法用一個(gè)字節(jié)來(lái)完全表示
- 要解決這個(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米间。
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è)字符趟章。
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è)字符返十。
用 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)單,只有二條:
對(duì)于單字節(jié)的符號(hào),字節(jié)的第一位設(shè)為0彬坏,后面7位為這個(gè)符號(hào)的unicode碼。因此對(duì)于英語(yǔ)字母,UTF-8編碼和ASCII碼是相同的。
對(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碼。
我們?cè)賮?lái)看看字符串“I am 君山”用 UTF-8 編碼:
可以看到?jīng)]有補(bǔ)0,沒(méi)有空間浪費(fèi)捎拯,相對(duì)的建芙,中文字用了3個(gè)字節(jié)存儲(chǔ)赶熟。“君”字UTF-8編碼成e5909b具體的編碼過(guò)程為:
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)物。