前文《為什么 String 要設(shè)計成 final ,又如何設(shè)計一個不可變類呢?》留下了一個編碼相關(guān)的問題,Java 中理論說是一個字符(漢字 字母)占用兩個字節(jié)。但是在 UTF-8 的時候 new String("字").getBytes().length 返回的是3 表示3個字節(jié),知道是為什么嗎,Java 中 char 占多少字節(jié)?
在回答這個問題之前,讓我們先學(xué)習(xí)一點基礎(chǔ)知識吧。
什么是字符集? 什么是編碼?
字符( Character )是文字與符號的總稱,包括文字、圖形符號、數(shù)學(xué)符號等。 一組抽象字符的集合就是字符集( Charset )。
之所以說“抽象”二字,是因為這里所提及的字符是不具任何具體形式的字符。例如“漢”這個字符,在文章中看到這個“漢”字,這其實是這個字符的一種具體表現(xiàn)形式,是它的圖像表現(xiàn)形式,當(dāng)人們讀“漢”這個字的時候,他們使用的是另一個具體表現(xiàn)形式---聲音。但是無論如何,這兩個表現(xiàn)形式都是指這個“漢”字,同一個字符的表現(xiàn)形式可能有無數(shù)種(點陣法、矢量法、音頻等),把每一種的表現(xiàn)形式下的同一個字符都納入到字符集中,會使得集合過于龐大。因此抽象字符集中的字符,都是指唯一存在的抽象字符,而忽略了它的具體表現(xiàn)形式。在給定一個抽象字符集合中的每個字符都分配了一個整數(shù)編號之后,這個字符集就有了順序,就成為了編碼字符集。同時,這個編號,可以唯一確定到底指的是哪一個字符。對于同一個字符,不同的字符集編碼系統(tǒng)所指定的整數(shù)編號也不盡相同。例如“兒”這個字,在 Unicode 中,它的編號是 0x513F,意思是它是 Unicode 這個編碼字符集中的第 0X513F 個字符。而在另一種編碼字符集中,這個字是 0xA449。
編碼字符集,指的是這種被分配了整數(shù)編號的字符集合,但是編碼字符集中字符被分配的整數(shù)編號,不一定就是該字符在計算機中存儲時所使用的值,計算機中存儲的字符到底使用什么二進制整數(shù)值來表示,由字符集編碼決定。
字符集編碼決定了如何將一個字符的整數(shù)編號對應(yīng)到一個二進制的整數(shù)值。英文字符幾乎所有的字符集編碼中,英文字母的整數(shù)編號與其在計算機內(nèi)部存儲的二進制形式都一致。但是有的編碼方式中,例如適用于 Unicode 字符集的 UTF-8 編碼形式,就將很大一部分字符的整數(shù)編號作了變換后存儲到計算機中。例如“漢”的 Unicode 值為 0x6C49, 但其編碼格式為 UTF-8 格式后的值為 0xE6B189 (3個字節(jié))。
編碼字符集里的每一個字符,都對應(yīng)到唯一的一個代碼值,這些代碼值叫做碼點( code point ),可以看做是這個字符在編碼字符集里的序號,字符在給定的編碼方式下的二進制比特序列稱為碼元( code unit )。
注意:我們在這里引出了兩個概念,碼點和碼元。
為什么要區(qū)分字符集與編碼這兩個概念?
在早期,字符集與編碼是一對一的。有很多的字符編碼方案,一個字符集只有唯一一個編碼實現(xiàn),兩者是一一對應(yīng)的。比如 GB2312,這種情況,無論你怎么去稱呼它們,比如“GB2312編碼”,“GB2312字符集”,說來說去其實都是一個東西,可能它本身就沒有特意去做什么區(qū)分,所以無論怎么說都不會錯。
到了 Unicode,變得不一樣了,唯一的 Unicode 字符集對應(yīng)了三種編碼:UTF-8,UTF-16,UTF-32。字符集和編碼等概念被徹底分離且模塊化,其實是 Unicode 時代才得到廣泛認(rèn)同的。
1)charset 是 character set 的簡寫,即字符集。
2)encoding 是 charset encoding 的簡寫,即字符集編碼,簡稱編碼。
從上圖可以很清楚地看到,
1、編碼是依賴于字符集的,就像代碼中的接口實現(xiàn)依賴于接口一樣;
2、一個字符集可以有多個編碼實現(xiàn),就像一個接口可以有多個實現(xiàn)類一樣。
為什么 Unicode 這么特殊?
搞出新的字符集標(biāo)準(zhǔn),無外乎是舊的字符集里的字符不夠用了。
Unicode 的目標(biāo)是統(tǒng)一所有的字符集,囊括所有的字符,因此再去整什么新的字符集就沒必要了。
但如果覺得它現(xiàn)有的編碼方案不太好呢?在不能弄出新的字符集情況下,只能在編碼方面做文章了,于是就有了多個實現(xiàn),這樣一來傳統(tǒng)的一一對應(yīng)關(guān)系就打破了。
從上圖可以看出,由于歷史方面的原因,你還會在不少地方看到把 Unicode 和 UTF-8 混在一塊的情況,這種情況下的 Unicode 通常就是 UTF-16 或者是更早的 UCS-2 編碼。
我們現(xiàn)在說了不少 Unicode,由于各種原因,必須承認(rèn),在不同的語境下,“Unicode”這個詞有著不同的含義。它可能指:
1)Unicode 標(biāo)準(zhǔn)
2)Unicode 字符集
3)Unicode 的抽象編碼(編號),也即碼點( code point )
4)Unicode 的一個具體編碼實現(xiàn),通常即為變長的 UTF-16,又或者是更早期的定長 16 位的 UCS-2。
這里重點介紹下 UTF-16 編碼,UTF-16 把 Unicode 字符集的碼點映射為 16 位長的整數(shù)(即碼元, 長度為 2 Byte)的序列,用于數(shù)據(jù)存儲或傳遞。Unicode 字符的碼點,需要 1 個或者 2 個 16 位長的碼元來表示,因此這是一個變長表示。
UTF-16 可看成是 UCS-2 的父集。在沒有輔助平面字符(基本思想是用 2 個 16 位的編碼表示一個字符,只對超過 65535 的字符這么做)前,UTF-16 與 UCS-2 指的是同一的意思。引入輔助平面字符后,就稱為 UTF-16 了。
現(xiàn)在若有軟件聲稱自己支持 UCS-2 編碼,那其實是暗指它不能支持在 UTF-16 中超過 2 bytes 的字集。對于小于 0x10000 的 UCS 碼,UTF-16 編碼就等于 UCS 碼。
為什么要重點介紹 UTF-16 編碼,因為 Java 的內(nèi)碼使用的是 UTF-16 編碼,也就是我們常說的 Unicode 編碼。
沒想到寫了那么長,只是介紹了字符集以及編碼的區(qū)別,看來是要分成兩篇文章才能回答前文留下的問題,本文總結(jié)其實就是兩句話:
編碼字符集里的每一個字符規(guī)定的順序,叫碼點( code point ),而這個字符在編碼字符集里的序號,在給定的編碼方式下的二進制序列叫碼元( code unit )。
在 Java 的世界里,我們更多接觸的外碼,即程序與外部交互時外部使用的字符編碼,而你不知道的還有更多,期待下期我們正式進入 Java 的編碼世界,最終去回答前文的那個問題。