Java - 什么是字符、码点和代理项?它们之间有什么区别?

52

我正试图找到“字符”、“代码点”和“代理项”的解释,虽然这些术语不仅限于Java,但如果有任何特定于语言的差异,我希望解释与Java相关。

我找到了一些关于字符和代码点之间差异的信息,其中字符是面向人类用户显示的内容,而代码点是编码该特定字符的值,但我对代理项一无所知。代理项是什么,它们与字符和代码点有何不同?我的字符和代码点的定义正确吗?

另一个线程中,关于将字符串作为字符数组遍历的特定评论引发了这个问题:“请注意,此技术提供的是字符,而不是代码点,这意味着您可能会得到代理项。” 我并没有真正理解,而不是在一个五年前的问题上创建一长串评论,我认为最好在一个新问题中请求澄清。


2
到目前为止,所有这些答案都增加了我对问题中术语的理解,因此虽然我会选择一个“答案”,但我认为它们都对我有所帮助。 - Alium Britt
5个回答

41

计算机中表示文本,需解决两个问题:首先需要将符号映射到数字,然后需要使用字节表示这些数字的序列。

码点(Code point)是标识符号的数字。两个众所周知的为符号分配数字的标准是ASCII和Unicode。 ASCII 定义了128个符号。 Unicode目前定义了109384个符号,比216还多得多。

此外,ASCII规定数字序列每个数字都用1个字节表示,而Unicode则指定了多种可能性,如UTF-8、UTF-16和UTF-32等。

当您尝试使用比表示所有可能值所需的位数更少的编码(例如UTF-16,它使用16位)时,您需要一些解决方法。

因此,代理项(Surrogates)是16位值,指示不能适合单个双字节值的符号。

Java在内部使用UTF-16表示文本。

特别地,char(字符)是一个无符号的两个字节的值,其中包含UTF-16值。

如果您想了解更多有关Java和Unicode的信息,我可以推荐这个通讯:第1部分第2部分


如果我没记错的话,8位等于1字节,这意味着UTF-8每个字符为1字节,UTF-16为2字节,UTF-32为4字节,对吗? - Alium Britt
@AliumBritt 不是那么容易。UTF-8/16大致相等,但机制不同。UTF-8是1-4个字节,而UTF-16是2个字节。 - Johan Sjöberg
@AliumBritt UTF-8和UTF-16尽可能使用1或2个字节,但对于更高的代码点,使用4个字节是不可避免的。 - Cephalopod
@Cephalopod - Nitpick: 严格来说,UTF-8的“代码点”可以长达6个字节……除了第5和第6个字节仅用于超出官方Unicode代码点空间的“平面”。(而他们已经表示永远不会去那里……) - Stephen C
@StephenC 我认为他们甚至可以使用七个字节,因为还有一个前缀位。澄清一下:使用4个字节,UTF-8可以编码2097151个代码点,是当前定义的代码点数量的20倍。因此,在不久的将来不会超过4个字节。 - Cephalopod
我错了。UTF-8的明确规范是Unicode 6.0.0,它明确定义了Unicode代码点范围的编码方式。5、6甚至7字节形式都是非标准的扩展。(根据维基百科页面的说法,扩展到7字节需要使用BOM中的一个字节...这将是一件坏事。) - Stephen C

20
你可以在Java类文档java.lang.Character中找到一个简短的解释:

Unicode字符表示

char数据类型(因此,Character对象所封装的值)基于最初的Unicode规范,该规范将字符定义为固定宽度的16位实体。Unicode标准后来更改为允许表示需要超过16位的字符。合法代码点​​的范围现在是U+0000U+10FFFF,称为Unicode标量值​​。[..]

U+0000U+FFFF范围内的字符有时被称为基本多语言平面(BMP)。其代码点大于U+FFFF的字符称为补充字符。Java平台在char数组和StringStringBuffer类中使用UTF-16表示。在此表示中,补充字符被表示为一对char值,第一个来自高代理项​​范围(\uD800-\uDBFF),第二个来自低代理项​​范围(\uDC00-\uDFFF)。

换句话说:

代码点通常表示单个字符。最初,类型char的值与Unicode代码点完全匹配。该编码也称为UCS-2

由于Unicode现在包含超过2^16个字符,因此将char定义为16位类型。为了支持整个字符集,编码从固定长度编码的UCS-2更改为可变长度编码的UTF-16。在这种编码中,每个码点由单个char或两个char表示。在后一种情况下,这两个字符被称为代理对。
UTF-16的定义方式是这样的,如果所有代码点都在2^14以下,则使用UTF-16和UCS-2编码的文本没有区别。也就是说,char可以用来表示一些但不是全部的字符。如果一个字符不能用单个char表示,那么术语char是具有误导性的,因为它只是作为16位字使用。

12

代码点通常指Unicode代码点。Unicode词汇表如下所述:

Codepoint(1):Unicode代码空间中的任何值;即从0到10FFFF16的整数范围。

在Java中,字符(char)是一个无符号的16位值;即0到FFFF。

正如您所看到的,有更多的Unicode代码点可以表示为Java字符。然而,Java需要能够使用所有有效的Unicode代码点来表示文本。

Java处理这个问题的方法是将大于FFFF的码点表示为字符(代码单元)对,即surrogate pair。这些对将一个大于FFFF的Unicode码点编码为一对16位值。这利用了Unicode代码空间的子范围(即D800到U+DFFF)保留了表示代理对的位置。有关技术细节请参见here
Java使用的编码方式的正确术语是UTF-16编码格式
另一个可能会看到的术语是代码单元,它是特定编码中使用的最小表示单位。在UTF-16中,代码单元为16位,对应于Java的char。其他编码(如UTF-8、ISO 8859-1等)具有8位代码单元,而UTF-32具有32位代码单元。
该术语“字符”有许多含义。在不同的上下文中,它意味着各种不同的事物。Unicode词汇表给出了Character的4个含义,如下所示:
引用: 字符。(1)书写语言中具有语义价值的最小组成部分;指抽象的含义和/或形状,而不是特定的形状(参见字形),但在代码表中,某种形式的视觉表示对读者的理解至关重要。 字符。(2)抽象字符的同义词。(Abstract Character。用于组织、控制或表示文本数据的信息单元。) 字符。(3)Unicode字符编码的基本编码单元。 字符。(4)源自中国的表意书写元素的英文名称。[请参阅表意符号(2)。]
然后还有Java特定的字符含义;即16位有符号数字(类型为char),可能表示UTF-16编码中的完整或部分Unicode代码点。

6
首先,Unicode是一种标准,试图定义和映射来自所有语言的所有单个字符,从英文字母到中文、数字、符号等。
基本上,Unicode具有一个长列表,其中代码点指的是编号。
简而言之:
  • 字符是文本中的单个标记,无论是字母、数字还是符号。
  • 代码点指的是Unicode标准中标记的编号。
  • 使用UTF-16编码方案表示的字符包含了如此多的字符,以至于不适合单个Java字符的分配空间。
  • 代理对是用来表示一个字符需要在一对字符空间中表示的术语。代理对是用来表示一个字符在Unicode表中所列得非常高,需要一对字符空间来表示它的术语。

在这种情况下,如果我所说的“代理”等同于“代理对”,因为如果我想要字符的表示,就总会有两个? - Alium Britt

5

简单来说:

  • 码元是一个占用2个字节的char,编码为UTF-16,每个字符不一定代表一个真实世界字符
  • 码点始终是一个真实世界字符,可能包含1个或2个码元,可以将其视为一个int,可能占据4个字节。

让代码(测试用例)说真话:
(需要Java 9 +,由于String的方法 codePoints()chars()

@Test
public void test() {
    String s = "Hi, 你好, おはよう, α-Ω\uD834\uDD1E"; // last real character is "", that takes 2 code unit,
    assertEquals(s.length(), s.toCharArray().length); // length() is based on char (aka code unit), not code point,

    System.out.printf("input string:\t\"%s\"%n%n", s);

    System.out.println("------ as code point (aka. real character) ------");
    // code point,
    s.codePoints().forEach(cp -> System.out.println(Character.toChars(cp)));
    assertEquals(s.codePoints().count(), s.length() - 1); // last read character takes 2 unit code,
    assertEquals(s.codePoints().count(), s.codePointCount(0, s.length())); // there is a method codePointCount() on String to get code point count on given char range,

    System.out.println("\n------ as char (aka. code unit) ------");
    // chars (aka. code unit),
    s.chars().forEach(c -> System.out.println(Character.toChars(c)));
    assertEquals(s.chars().count(), s.length()); // string length is the count of code unit, not code point,
}

输出:

输入字符串:"Hi,你好,おはよう,α-Ω"
------ 作为代码点(即真实字符)------
H i , 你 好 , お は よ う , α - Ω
------ 作为字符(即代码单元)------
H i , 你 好 , お は よ う , α - Ω ? ?

最后一个真实字符是,它占用了2个代码单元\uD834\uDD1E,它是一个单一的代码点,当尝试分别打印这两个代码单元时,它们无法被识别,并且显示每个代码单元的?


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接