UTF-8 エンコードとバイト判定


UTF-8 の体系

UTF-8では、Unicode文字を 1~4 バイトの可変長で表現する符号化方式です。 U+0000 から U+007f つまり、0-127 の範囲は US-ASCII と互換性があります。

各Unicode範囲において以下のような割当となります。

Unicode 1 2 3 4
U+0000 - U+007f 0xxx-xxxx
U+0080 - U+07ff 110x-xxxx 10xx-xxxx
U+0800 - U+ffff 1110-xxxx 10xx-xxxx 10xx-xxxx
U+10000 - U+10ffff 1111-0xxx 10xx-xxxx 10xx-xxxx 10xx-xxxx

先頭バイトのビットパターンを見れば、続くバイト数が判別できるようになっています。 下位バイトは先頭が 10 のビットパターンになります。

なお、UTF-8で符号されたテキストデータにBOM(Byte Order Mark)は不要ですが、先頭に 0xEF 0xBB 0xBF (11101111 10111011 10111111) としてBOMを付加する場合もあります。 ただし通常 BOM を付与するケースはないでしょう。


UTF-8 エンコード

U+3042 「あ」 を例にとり、UTF-8 のエンコードについて見ていきましょう。

U+3042U+0800 - U+ffff の範囲に該当するため UTF-8 では3バイトで表現します。

U+3042を2進数で表すと以下になります。

0011 0000 0100 0010

下位から6ビットずつ分割すると以下のようになります。

0011  000001  000010

それぞれ定められたビットパターン 1110 10 10 を付与します。

1110 0011  10 000001   10 000010
  ↓
1110 0011  1000 0001   1000 0010

これで UTF-8 エンコード が完了し、16進で e3 81 82 となります。


Java では以下のように書くことができます。

public static byte[] toUtf8(int cp) {

    int mask = 0x3F; // 6bit mask 0011 1111

    if (0x0000 <= cp && cp <= 0x007f) {
        return new byte[] { (byte) cp };

    } else if (0x0080 <= cp && cp <= 0x07ff) {
        return new byte[] {
            (byte) (0xC0 | (cp >>> 6)),
            (byte) (0x80 | (cp & mask)) };

    } else if (0x0800 <= cp && cp <= 0xffff) {
        return new byte[] {
            (byte) (0xE0 | (cp >>> 12)),
            (byte) (0x80 | (cp >>>  6 & mask)),
            (byte) (0x80 | (cp & mask)) };

    } else if (0x10000 <= cp && cp <= 0x10ffff) {
        return new byte[] {
            (byte) (0xF0 | (cp >>> 18)),
            (byte) (0x80 | (cp >>> 12 & mask)),
            (byte) (0x80 | (cp >>>  6 & mask)),
            (byte) (0x80 | (cp & mask)) };
    } else {
        throw new IllegalStateException("Illegal code point. " + cp);
    }

}


UTF-8 のバイト判定

UTF-8 では、先頭バイトを見ることで続くバイト数が判断できます。

  • 0xxx-xxxx : 1バイト
  • 110x-xxxx : 2バイト
  • 1110-xxxx : 3バイト
  • 1111-0xxx : 4バイト

バイト列の先頭のビットパターンを使えば、それに続くバイト数を判定することができます。

public static short countUtf8Byte(byte b) {
    if ((b & 0x80) == 0x00) {
        return 1;
    } else if ((b & 0xE0) == 0xC0) {
        return 2;
    } else if ((b & 0xF0) == 0xE0) {
        return 3;
    } else if ((b & 0xF8) == 0xF0) {
        return 4;
    } else {
        return 0;
    }
}

byte のシフト演算は int にワイドニング変換されるので、シフト演算ではなくビット論理演算を利用します。

特定バイトが UTF-8 の下位バイトかどうかは、先頭ビットが 10(10xx-xxxx) で始まるかを見れば良いので以下のように判断できます。

public static boolean isUtf8Lower(byte b) {
    return (b & 0xC0) == 0x80;
}