Unicode 的實作之一,就是用兩個位元組來儲存所有字元,這在 Unicode 與 UTF 有看過例子,但很顯然的,對於英文字母這種 ASCII 可包含的字元,基本上只需要一個位元組就可以表達,使用 UTF-16,高位元組的部份基本上都是 0,其實蠻耗費儲存空間。
Unicode 的實作方式之一 UTF-8(8-bit Unicode Transformation Format),使用可變長度位元組的方式來儲存字元,一個字元的儲存長度可從一個位元組到四個位元組。
舉個例子來說,如果用 UTF-8 的方式儲存英文字 母,則只會使用一個位元組,如果儲存中文字,則會用三個位元組。例如在一個 UTF-8 中若同時儲存「Test測試」,則結果如下:
54、65、73、74 分別是 T、e、s、t 四個字元的位元組資料,而 e6、b8、ac 是「測」字元的三個位元組資料,e8、a9、a6 是「試」字元的三個位元組資料。
由於對 ASCII 字元,UTF-8 仍用一個位元組儲存,UTF-8 對於原本就使用 ASCII 的系統來說,既有的資料並不用作什麼或很少修改,就可以與 UTF-8 一起使用,對於需要多國語系支援的系統來說,經常採用 UTF-8 作為預設方案。
要注意的是,如果使用 Windows 舊版記事本儲存時,選項採用「UTF-8」,記事本會在檔案開頭置入EF、BB、BF 三個位元組,作為位元組順序記號(Byte-Order Mark,BOM),表示這是一個 UTF-8 編碼檔案。如果用可檢視十六進位的編輯器來看,就可以看到:
Unicode 標準雖允許為 UTF-8 檔案標識 BOM,但其實不需要,因為 UTF-8 沒有位元組順序問題,也不建議在 UTF-8 檔案標識 BOM(只是為了標識這是一個 UTF-8 編碼檔案),而且許多程式並不預期有 UTF-8 檔案前的 BOM。
例如若儲存 Java 原始碼時使用 Windows 記事本存為「UTF-8」,則使 用 javac
編譯器時就會出問題,因為 javac
編譯器並不處理 BOM,必須改用可儲存 UTF-8 時檔首無 BOM 的編輯器,javac
才可以正確進行編 譯。
(Windows 新版的記事本,預設使用檔首無 BOM 的 UTF-8 儲存。)
由於 UTF-8 採可變長度位元組來儲存字元,必須有個方式,識別位元組是否為 ASCII 字元,或者哪幾個位元組該視為一個字元的資料,基本規則可在維基百科 UTF-8 說明中的「UTF-8編碼位元組含義」找到。
舉例來說,對於「測」這個字來說,用十六進位制來檢視:
第一個位元組為 e6,二進位表示就是 11100110,前三個位元都是 1,第四位為 0,表示這個位元組是非 ASCII 字元的第一個位元組,而且這個字元用了三個位元組,所以接下來要讀入 b8(10111000)與 ac(10101100)兩個位元組,可以看到,接下來這兩個位元組的第一個位元是 1,第二個 位元是 0,各表示它們是非 ASCII 字元的位元組資料其中一個位元組。
下面這個範例是個簡單的 UTF-8 讀取程式:
package cc.openhome;
import static java.lang.System.out;
import java.nio.file.Files;
import java.nio.file.Paths;
public class Main {
public static void main(String[] args) throws Exception {
byte[] bytes = Files.readAllBytes(Paths.get("sample.txt"));
int i = 0;
while(i < bytes.length) {
int length = byteLength(bytes[i]);
print(bytes, i, length);
i += length;
}
}
private static int byteLength(byte b) {
if(b >= 0) { // ASCII 字元
return 1;
}
else if(b >= -16) { // 四個位元組字元
return 4;
}
else if(b >= -32) { // 三個位元組字元
return 3;
}
else if(b >= -64) { // 兩個位元組字元
return 2;
}
throw new RuntimeException("未知字元");
}
private static void print(byte[] origin, int begin, int length) throws Exception {
byte[] bs = from(origin, begin ,length);
out.printf("%s\t", new String(bs, "UTF-8"));
for(byte b : bs) {
out.printf("%-3h", b & 0x00FF);
}
out.println();
}
private static byte[] from(byte[] origin, int begin, int length) {
byte[] bytes = new byte[length];
for(int i = 0; i < length; i++) {
bytes[i] = origin[begin + i];
}
return bytes;
}
}
如果有個 sample.txt 儲存為檔首無 BOM 的 UTF-8 文件,內容為「這T是e個s測t試」,用上面這個程式讀取,結果會如下:
這 e9 80 99
T 54
是 e6 98 af
e 65
個 e5 80 8b
s 73
測 e6 b8 ac
t 74
試 e8 a9 a6
可以對照 sample.txt 的十六進位檢視結果: