不少人常常分不清UTF-8編碼和UTF-16編碼,或常常會問"Unicode編碼和UTF-8編碼有什麼區別聯繫","Java的外碼內碼又是什麼東西",這篇文章主要作一個關於編碼知識的簡單掃盲,包括對一些常見概念混淆進行區分講解。java
咱們常常提到的關於編碼的概念能夠粗略劃分爲兩類:數組
Unicode字符集一開始提出的時候,認爲碼值範圍爲0-65535(0-FFFF,這一段區域也被稱爲Basic Multilingual Plane, 簡稱BMP)就能夠表示全部的字符,但隨着時代發展,0-FFFF也不夠容納全部字符,所以Unicode劃出了一個代理區:D800-DFFF, Unicode標準規定U+D800 - U+DFFF的值不對應於任何字符。這也是爲何有些人說:有些字符須要用兩個Unicode字符去表示的緣由。函數
目前Unicode的編碼空間爲0-10FFFF,根據第一段落能夠得知,當某個字符的Unicode碼值落在0-FFFF時,則只用一個Unicode字符便可表示,不然就會用兩個。編碼
Utf-8全稱爲8-bit Unicode Transformation Format,是一種針對Unicode字符集的可變長編碼,不一樣的Unicode碼點會使用不一樣的字節數去存儲,如ascii碼(都小於128)則會使用1個字節去存儲,一些經常使用字符(如部分中文)會使用2~3個字節去存儲,這有一些優點,首先對於ascii碼徹底兼容,且對於某些場景(只存在ascii碼)編碼後佔用空間少,缺點也很明顯,當遇到的都是須要佔用3個字節存儲的Unicode碼點時,則會耗費更大的空間。代理
utf-8編碼的基本規則以下:code
Unicode碼範圍 | Utf-8編碼格式 |
---|---|
0x0000-0x007F(0~127) | 0xxxxxxx |
0x0080-0x07FF(128~2047) | 110xxxxx 10xxxxxx |
0x0800-0xFFFF(2048-65535) | 1110xxxx 10xxxxxx 10xxxxxx |
0x10000-0X10FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
首先將Unicode碼用二進制表示,而後根據其所屬的Unicode範圍對應的Utf-8編碼格式截取最後幾位。orm
如0x0000-0x00FF對應的Utf-8編碼格式是0xxxxxxx,則截取最後7位(有效表示位其實也只有7位,由於從右往左第8位必定是0),對應填到x中。對象
同理,0x0080-0x07FF(0000 0000 1000 0000 — 0000 0111 1111 1111),則截取後11位(有效表示位其實只有11位,由於從右往左第12位必定是0),對應填到x中。utf-8
0x0080-0xFFFF(0000 1000 0000 0000 — 1111 1111 1111 1111),截取後15位(有效表示位其實只有16位,由於從右往左第17位必定是0),對應填到x中。ci
0x100000-0X10FFFF(0000 0001 0000 0000 0000 — 0001 0000 1111 1111 1111 1111),截取後21位(有效表示位其實只有21位,從右往左第22位必定是0),對應填到x中。
所以,咱們能夠獲得全部Unicode的Utf-8編碼規則。
在Unicode字符集中講到,Unicode字符集存在一個拓展區域:D800-DFFF,用於表示碼點在0x10000-0x10FFFF範圍的字符。當碰到某個在該範圍內的Utf-16字符,須要再讀一個Utf-16字符,將兩個Utf-16字符組合表示一個Unicode字符。
Unicode碼範圍 | Utf-16編碼格式 |
---|---|
0x0000-0xFFFF(0~65535) | 使用2個字節存儲 |
0x10000-0x10FFFF(65536~) | 使用4個字節存儲,須要利用上述提到的代理區 |
接下來將Unicode碼用二進制表示,嘗試將它用Utf-16編碼格式進行編碼。
對應0x0000-0xFFFF範圍的Unicode碼,直接將這16爲對應填入兩個字節(剛好16位)就能夠獲得Utf-16編碼。
而對應0x10000-0x10FFFF的Unicode碼,須要有一些特殊處理:
經過處理,前導代理和後導代理剛好佔滿了0xD800—0xDFFF這一段代理區域,這樣處理的一個優勢在於,看到每個Utf-16編碼,能夠很清楚地肯定它是屬於前導代理、後導代理仍是除此之外的BMP區域中的Unicode。
MUTF-8(Modified UTF-8)編碼,能夠認爲是對UTF-16編碼的再編碼。它的編碼方式與UTF-8編碼很是類似,只須要記住某些不一樣的狀況,其餘都與UTF-8編碼一致。
具體的不一樣狀況有二:
因此網上常常會提到UTF-8編碼,又提到用1—6個字節去編碼,其實說的是MUTF-8編碼。
Java的內碼是UTF-16,外碼是MUTF-8。那什麼是內碼和外碼呢?
內碼:程序內部使用的字符編碼,如java的char,因此java的char是2字節16位;
外碼:程序外部交互時使用的字符編碼,如class文件。
在深刻理解Java虛擬機第三版6.3.2節中,咱們能夠得知其實Java的字符串常量(如String str="hello world")都是以CONSTANT_Utf8_info類型存在常量池中的,class文件的編碼是MUTF-8,因此CONSTANT_Utf8_info中存儲的根據不一樣的實現通常是存儲MUTF-8字節數組或UTF-16字符數組,每次構建時java.lang.String對象時,須要經過MUTF-8=>UTF-16的一個編碼轉換將外碼轉爲內碼,再將其塞到char數組(value)中。
在Java API層對字符串的操做,其實通常也是對UTF-16字符的操做,如charAt函數:
public char charAt(int index) { if ((index < 0) || (index >= value.length)) { throw new StringIndexOutOfBoundsException(index); } return value[index]; }
charAt函數實際是返回了一個char,因此是返回了一個UTF-16字符,它不必定是一個完整的Unicode碼點。
固然,在Java API層也可使用getBytes(」UTF-8")則是返回UTF-8編碼的字節數組。
根據上面文章的講解,咱們就能夠講清楚下面幾個經常遇到的問題:
另外,java虛擬機對字符串的表示或處理不少都是使用的UTF-16編碼或MUTF-8編碼,而UTF-8編碼通常是顯式經過Java API層的String.getBytes("UTF-8")
函數獲得。
平時使用時,若是隻用Java語言開發通常不會有什麼亂碼問題,但若是本身想手動實現一個Java虛擬機,或是要經過JNI作一些事情的時候,就須要去了解一下Java的這些編碼知識了。如筆者以前參與的項目,用go處理Java虛擬機的編碼問題就很頭大,由於Go這邊默認使用的是UTF-8編碼,因此若是在實現常量池的過程當中用Go的string去存儲java.lang.String的實際內容,則可能出現一些奇怪的亂碼問題。