ZetCode

Java 字素

最后修改于 2025 年 5 月 24 日

在本文中,我们将使用 Java 中的字素。 字素是任何给定语言的书写系统中最小的单位。

字素

字素是任何给定语言的书写系统中最小的基本单位。 它可能由单个字符或多个组合元素组成,例如重音符号、变音符号或连字,形成视觉上不同的符号。 虽然字素本身可能并不总是传达含义,但它在表示语言结构方面起着至关重要的作用。

从历史上看,术语“字符”用于指原始 ASCII 表中的单个符号,该表仅支持有限的字符集。 随着数字文本表示的发展,引入了 Unicode 标准来适应书面语言的巨大多样性,从而能够表示复杂的脚本、表情符号和多字符字素。

Unicode

Unicode 是一种全球公认的标准,可确保在不同的书写系统中一致地编码、表示和处理文本。 它的开发旨在统一字符表示,从而在各种语言、脚本和专用符号(如数学符号和表情符号)中实现无缝文本处理。

在 Java 中,字符串是一个 Unicode 字符序列,充当结构化数据类型,将文本存储为一系列值,通常以字节表示。 Java 内部使用 UTF-16 编码,它支持直接表示大多数字符,同时使用代理对来扩展对超出基本多文种平面 (BMP) 的 Unicode 字符的支持。

除了 ASCII 字符集之外,使用术语“字素”而不是“字符”更合适。 Java 提供了 BreakIterator 类,该类允许迭代字符串中的文本元素(字素)。 文本元素可能由基本字符、代理对或组合字符序列组成,它们共同形成单个显示的符号。

代码点 表示分配给 Unicode 字符的数字标识符。 例如,拉丁字母 Q 对应于 U+0051 代码点,而西里尔小写字母 ж 由 U+0436 表示。 Unicode 为每个字符分配唯一的代码点,确保在各种系统中的文本处理具有普遍兼容性。

字素簇 由一个或多个代码点组成,这些代码点在视觉上组合形成单个图形单元。 例如,印地语音节 ते 由两个代码点组成:U+0924 代表 U+0947 代表 。 正确处理字素簇对于准确呈现复杂脚本至关重要。

字符串的实际存储表示由字节组成,这些字节对每个代码点进行编码。 根据 Unicode 编码方案(例如 UTF-8UTF-16UTF-32),每个代码点可能需要不同数量的字节才能在内存或存储中完全表示。

Unicode 表

下面是 Unicode 表的一小部分,显示了代码点、字符及其名称

代码点字符名称
U+0041A拉丁大写字母 A
U+0061a拉丁小写字母 A
U+03B1α希腊小写字母 ALPHA
U+0416Ж西里尔大写字母 ZHE
U+05D0א希伯来语字母 ALEF
U+0924天城文字母 TA
U+1F600😀咧嘴笑脸

代理对

Java 采用 UTF-16 编码方案 来存储 Unicode 文本,使用 16 位(双字节)代码单元来表示每个字符。 此方案支持 基本多文种平面 (BMP) 中的所有字符,范围从 U+0000U+FFFF,其中包括广泛使用的脚本,如拉丁语、希腊语、西里尔语、阿拉伯语、希伯来语、汉语、日语、韩语,以及符号和标点符号。

对于 BMP 之外的 Unicode 字符,范围从 U+10000U+10FFFF,Java 使用 代理对 来扩展 UTF-16 编码功能,从而能够表示这些附加字符。

代理对 是一对 16 位代码单元,它们共同表示 BMP 之外的单个 Unicode 字符。 第一个单元称为高位代理,后跟低位代理。 这种机制使 Java 能够处理扩展字符,例如表情符号、历史脚本和各种领域中使用的专用符号。

Unicode 还支持 组合字符序列,其中基本字符由一个或多个组合字符(例如变音符号)修改。 当与形成单个视觉单元的其他代码点组合时,代理对可以表示独立字符或成为更大的字素簇的一部分。

代码点字符高位代理低位代理名称
U+1F600😀D83DDE00咧嘴笑脸
U+1F680🚀D83DDE80火箭
U+1F4A9💩D83DDCA9一堆便便
U+1F60D😍D83DDE0D带爱心的笑脸
U+1F47B👻D83DDC7B鬼魂

在上表中,代码点 U+1F600、U+1F680、U+1F4A9、U+1F60D 和 U+1F47B 表示为代理对。 高位代理是第一个代码单元 (D83D),低位代理是第二个代码单元 (DE00、DE80、DCA9、DE0D、DC7B)。 这些对允许表示基本多文种平面 (BMP) 之外的字符,其中包括许多表情符号和其他特殊字符。

Java 字素示例

在以下示例中,我们将使用字素。

Main.java
void main() {

    System.out.println("The Hindi word Namaste");

    String word = "नमस्ते";
    System.out.println(word);
    System.out.println();

    // code points
    System.out.println("Code points:");
    for (int i = 0; i < word.length(); i += Character.charCount(word.codePointAt(i))) {
        int cp = word.codePointAt(i);
        System.out.printf("U+%04X %s%n", cp, new String(Character.toChars(cp)));
    }
    System.out.println();

    // bytes
    System.out.println("Bytes:");
    byte[] bytes = word.getBytes(StandardCharsets.UTF_8);

    for (byte b : bytes) {
        System.out.printf("%d ", b & 0xFF); // Convert signed byte to unsigned
    }

    System.out.println("\n");

    // graphemes
    System.out.println("Graphemes:");
    BreakIterator graphemeIter = BreakIterator.getCharacterInstance();
    graphemeIter.setText(word);

    int count = 0;
    int start = graphemeIter.first();

    while (start != BreakIterator.DONE) {

        int end = graphemeIter.next();
        if (end == BreakIterator.DONE) {
            break;
        }

        String grapheme = word.substring(start, end);
        System.out.println(grapheme);
        count++;
        start = end;
    }

    System.out.printf("the word has %d graphemes%n", count);
}

该示例定义了一个变量,其中包含一个印地语单词 namaste。 我们打印该单词,打印其代码点、字节,并打印和计数字素的数量。

String word = "नमस्ते";
System.out.println(word);

我们有印地语单词 namaste; 我们将其打印到控制台。

System.out.println("Code points:");

for (int i = 0; i < word.length(); i += Character.charCount(word.codePointAt(i))) {
    int cp = word.codePointAt(i);
    System.out.printf("U+%04X %s%n", cp, new String(Character.toChars(cp)));
}

我们打印单词的代码点。 length 方法确定字符串中 UTF-16 字符的数量。 Character.charCount 方法用于确定表示代码点所需的字符值的数量。 如果是代理对,我们需要更多字节来表示一个字素。

使用 word.codePointAt 方法,我们获取指定索引处的 Unicode 代码点。 Character.toChars 方法将代码点转换为 char 数组,我们将其转换为字符串以打印字素。

System.out.println("Bytes:");
byte[] bytes = word.getBytes(StandardCharsets.UTF_8);

for (byte b : bytes) {
    System.out.printf("%d ", b & 0xFF); // Convert signed byte to unsigned
}

我们打印存储在磁盘上的单词的实际字节。 我们使用带有 StandardCharsets.UTF_8getBytes 方法来获取底层字节数组。

System.out.println("Graphemes:");
BreakIterator graphemeIter = BreakIterator.getCharacterInstance();
graphemeIter.setText(word);

int count = 0;
int start = graphemeIter.first();

while (start != BreakIterator.DONE) {

    int end = graphemeIter.next();

    if (end == BreakIterator.DONE)  {
        break;
    }

    String grapheme = word.substring(start, end);
    System.out.println(grapheme);
    count++;
    start = end;
}

我们打印单词的字素并对其进行计数。 BreakIterator.getCharacterInstance 用于枚举单词的字素。 substring 方法提取起始索引和结束索引之间的字素。

$ java Main.java
The Hindi word Namaste
नमस्ते

Code points:
U+0928 न
U+092E म
U+0938 स
U+094D ्
U+0924 त
U+0947 े

Bytes:
224 164 168 224 164 174 224 164 184 224 165 141 224 164 164 224 165 135 

Graphemes:
न
म
स्ते
the word has 3 graphemes

输出显示了单词、其代码点、字节和字素。 字素作为单独的行打印,并且总字素数显示在末尾。 在这种情况下,单词 नमस्ते 由四个字素组成:न、म、स 和 ते。 字素 ते 是基本字符 त 和组合字符 े 的组合,它们共同形成单个字素。

但是,在这种情况下,Java 错误地计算了字素。

来源

Java BreakIterator

用于处理 Unicode 的有用网站:www.fileformat.infowww.utf8-chartable.de

在本文中,我们使用了 Unicode 字符串的字素、代码点、字节和代理对。

作者

我叫 Jan Bodnar,是一位充满激情的程序员,拥有丰富的编程经验。 我自 2007 年以来一直在撰写编程文章。 迄今为止,我已经撰写了 1,400 多篇文章和 8 本电子书。 我在编程教学方面拥有十多年的经验。

列出所有Java教程