Java UTF-8

更新于 2025-12-26

Jakob Jenkov 2022-08-07

UTF-8 是一种用于编码 Unicode 字符的字节编码方式。UTF-8 使用 1、2、3 或 4 个字节来表示一个 Unicode 字符。请记住,Unicode 字符由 Unicode 码点(code point) 表示。因此,UTF-8 使用 1 到 4 个字节来表示一个 Unicode 码点。

UTF-8 是 Web 上最常用的文本编码之一,因此非常流行。Web 浏览器支持 UTF-8。许多编程语言也允许你在代码中使用 UTF-8,并能轻松地导入和导出 UTF-8 文本。多种文本数据格式和标记语言通常也采用 UTF-8 编码,例如 JSON、XML、HTML、CSS、SVG 等。

UTF-8 的标记位与码点位

在将 Unicode 码点转换为一个或多个 UTF-8 编码字节时,每个字节都由**标记位(marker bits)码点位(code point bits)**组成。标记位用于指示如何解释该字节,而码点位则用于表示码点的值。

在以下各节中,标记位用 01 表示,码点位用字符 ZYXWV 表示。每个字符代表一个比特位。

UTF-8 中使用的 Unicode 码点区间

  • 对于十六进制区间 U+0000 到 U+007F 的 Unicode 码点,UTF-8 使用 1 个字节 表示字符。此区间的码点与 ASCII 字符相同,并使用相同的整数值(码点)表示。其二进制形式如下:

    0ZZZZZZZ
    

    其中,标记位为最高位的 0Z 表示码点值的位。

  • 对于区间 U+0080 到 U+07FF 的码点,UTF-8 使用 2 个字节 表示字符。二进制形式如下:

    110YYYYY 10ZZZZZZ
    

    标记位为 11010YZ 表示码点值的位。左边是高位字节(most significant byte)。

  • 对于区间 U+0800 到 U+FFFF 的码点,UTF-8 使用 3 个字节 表示字符。二进制形式如下:

    1110XXXX 10YYYYYY 10ZZZZZZ
    

    标记位为 1110 和两个 10XYZ 表示码点值的位。

  • 对于区间 U+10000 到 U+10FFFF 的码点,UTF-8 使用 4 个字节 表示字符。二进制形式如下:

    11110VVV 10WWXXXX 10YYYYYY 10ZZZZZZ
    

    标记位为 11110 和三个 10VW 表示字符所在的 Unicode 平面(plane),其余 XYZ 表示码点的其余部分。

读取 UTF-8

读取 UTF-8 编码字节并还原为字符时,需先判断一个字符(码点)是由 1、2、3 还是 4 个字节表示的。方法是检查第一个字节的位模式:

  • 若第一个字节形如 0ZZZZZZZ(最高位为 0),则该字符仅由这一个字节表示。
  • 若第一个字节形如 110YYYYY(前三位为 110),则该字符由两个字节表示。
  • 若第一个字节形如 1110XXXX(前四位为 1110),则该字符由三个字节表示。
  • 若第一个字节形如 11110VVV(前五位为 11110),则该字符由四个字节表示。

确定字节数后,提取所有携带码点信息的位(即 VWXYZ 所代表的位),将其组合成一个 32 位整数(例如 Java 中的 int)。例如,读取一个 4 字节 UTF-8 字符后的 32 位结构如下:

000000 000VVVWW XXXXYYYY YYZZZZZZ

注意:所有标记位(如 1111010)已被移除,仅保留原始码点位。

写入 UTF-8

写入 UTF-8 文本时,需要将 Unicode 码点转换为 UTF-8 编码的字节序列。步骤如下:

  1. 根据码点值确定所需字节数(参考上文的区间划分)。
  2. 将码点的位按规则填入对应字节的码点位中,并添加相应的标记位。

例如,一个需要 4 字节表示的码点,其抽象位模式为:

000000 000VVVWW XXXXYYYY YYZZZZZZ

对应的 UTF-8 字节序列为:

11110VVV 10WWXXXX 10YYYYYY 10ZZZZZZ

在 Java 中读写 UTF-8

Java 提供了多种读写 UTF-8 字节的方式。

将 UTF-8 读入 Java String

byte[] utf8 = ... // 从文件、URL 等获取 UTF-8 字节数组
String string = new String(utf8, StandardCharsets.UTF_8);

从 Java String 获取 UTF-8 字节

byte[] utf8 = string.getBytes(StandardCharsets.UTF_8);

一个可读写 UTF-8 码点的 Utf8Buffer

以下是一个既能写入也能读取 UTF-8 码点(以 Java int 表示)的 Utf8Buffer 类:

public class Utf8Buffer {
    public byte[] buffer;
    public int offset;
    public int length;
    public int endOffset;
    public int tempOffset;

    public Utf8Buffer(byte[] data, int offset, int length) {
        this.buffer = data;
        this.offset = offset;
        this.tempOffset = offset;
        this.length = length;
        this.endOffset = offset + length;
    }

    public void reset() {
        this.tempOffset = this.offset;
    }

    public void calculateLengthAndEndOffset() {
        this.length = this.tempOffset - this.offset;
        this.endOffset = this.tempOffset;
    }

    public int writeCodepoint(int codepoint) {
        if (codepoint < 0x00_00_00_80) {
            buffer[this.tempOffset++] = (byte) (0xFF & codepoint);
            return 1;
        } else if (codepoint < 0x00_00_08_00) {
            buffer[this.tempOffset] = (byte) (0xFF & (0b1100_0000 | (0b0001_1111 & (codepoint >> 6))));
            buffer[this.tempOffset + 1] = (byte) (0xFF & (0b1000_0000 | (0b0011_1111 & codepoint)));
            this.tempOffset += 2;
            return 2;
        } else if (codepoint < 0x00_01_00_00) {
            buffer[this.tempOffset] = (byte) (0xFF & (0b1110_0000 | (0b0000_1111 & (codepoint >> 12))));
            buffer[this.tempOffset + 1] = (byte) (0xFF & (0b1000_0000 | (0b0011_1111 & (codepoint >> 6))));
            buffer[this.tempOffset + 2] = (byte) (0xFF & (0b1000_0000 | (0b0011_1111 & codepoint)));
            this.tempOffset += 3;
            return 3;
        } else if (codepoint < 0x00_11_00_00) {
            buffer[this.tempOffset] = (byte) (0xFF & (0b1111_0000 | (0b0000_0111 & (codepoint >> 18))));
            buffer[this.tempOffset + 1] = (byte) (0xFF & (0b1000_0000 | (0b0011_1111 & (codepoint >> 12))));
            buffer[this.tempOffset + 2] = (byte) (0xFF & (0b1000_0000 | (0b0011_1111 & (codepoint >> 6))));
            buffer[this.tempOffset + 3] = (byte) (0xFF & (0b1000_0000 | (0b0011_1111 & codepoint)));
            this.tempOffset += 4;
            return 4;
        }
        throw new IllegalArgumentException("Unknown Unicode codepoint: " + codepoint);
    }

    public int nextCodepoint() {
        int firstByteOfChar = 0xFF & buffer[tempOffset];
        if (firstByteOfChar < 0b1000_0000) {
            tempOffset++;
            return firstByteOfChar;
        } else if (firstByteOfChar < 0b1110_0000) {
            int nextCodepoint = 0b0001_1111 & firstByteOfChar;
            nextCodepoint <<= 6;
            nextCodepoint |= 0b0011_1111 & (0xFF & buffer[tempOffset + 1]);
            tempOffset += 2;
            return nextCodepoint;
        } else if (firstByteOfChar < 0b1111_0000) {
            int nextCodepoint = 0b0000_1111 & firstByteOfChar;
            nextCodepoint <<= 6;
            nextCodepoint |= 0x3F & buffer[tempOffset + 1];
            nextCodepoint <<= 6;
            nextCodepoint |= 0x3F & buffer[tempOffset + 2];
            tempOffset += 3;
            return nextCodepoint;
        } else if (firstByteOfChar < 0b1111_1000) {
            int nextCodepoint = 0b0000_0111 & firstByteOfChar;
            nextCodepoint <<= 6;
            nextCodepoint |= 0x3F & buffer[tempOffset + 1];
            nextCodepoint <<= 6;
            nextCodepoint |= 0x3F & buffer[tempOffset + 2];
            nextCodepoint <<= 6;
            nextCodepoint |= 0x3F & buffer[tempOffset + 3];
            tempOffset += 4;
            return nextCodepoint;
        }
        throw new IllegalStateException("Codepoint not recognized from first byte: " + firstByteOfChar);
    }
}

使用示例:

Utf8Buffer utf8Buffer = new Utf8Buffer(new byte[1024], 0, 0);
utf8Buffer.writeCodepoint(0x7F);

// 写入后需计算长度和偏移量;若要读取,需重置 tempOffset
utf8Buffer.calculateLengthAndEndOffset();
utf8Buffer.reset();

int nextCodePoint = utf8Buffer.nextCodepoint();

在 UTF-8 中向前搜索

在 UTF-8 中向前搜索相对直接:逐个解码字符并与目标字符比较即可。

在 UTF-8 中向后搜索

UTF-8 编码的一个优点是支持向后搜索。通过检查每个字节的标记位,可以判断它是否是一个字符的起始字节:

  • 起始字节的标记模式:

    • 0xxxxxxx:1 字节字符(也是 ASCII)
    • 110xxxxx:2 字节字符开头
    • 1110xxxx:3 字节字符开头
    • 11110xxx:4 字节字符开头
  • 非起始字节的标记模式:

    • 10xxxxxx:2/3/4 字节字符的后续字节

因此,在向后搜索时,只需不断回退,直到找到一个起始字节,然后向前解码整个字符并进行比对。