UTF-8


Unicode 的實作之一,就是用兩個位元組來儲存所有字元,這在 Unicode 與 UTF 有看過例子,但很顯然的,對於英文字母這種 ASCII 可包含的字元,基本上只需要一個位元組就可以表達,使用 UTF-16,高位元組的部份基本上都是 0,其實蠻耗費儲存空間。

Unicode 的實作方式之一 UTF-8(8-bit Unicode Transformation Format),使用可變長度位元組的方式來儲存字元,一個字元的儲存長度可從一個位元組到四個位元組。

舉個例子來說,如果用 UTF-8 的方式儲存英文字 母,則只會使用一個位元組,如果儲存中文字,則會用三個位元組。例如在一個 UTF-8 中若同時儲存「Test測試」,則結果如下:

UTF-8

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 編碼檔案。如果用可檢視十六進位的編輯器來看,就可以看到:

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編碼位元組含義」找到。

舉例來說,對於「測」這個字來說,用十六進位制來檢視:

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 的十六進位檢視結果:

UTF-8