Java Unicode编码

42

Java中的char2个字节(最大大小为65,536),但有95,221个Unicode字符。这是否意味着在Java应用程序中无法处理某些Unicode字符?

这是否归结于您使用的字符编码?

7个回答

40

1
上面链接的页面是我读过的最清晰的不同编码、JVM API使用、某些措辞的含义(“代码点” vs. “代码单元”)以及JNI提供的区别的页面之一。 - Allen George
以下网站非常清晰但相当详细。它甚至超越了代码点的定义,并展示了如何处理和计算图形符号(完整呈现的字符,可能由多个代码点组成,当使用组合变音标记时)。http://illegalargumentexception.blogspot.jp/2009/05/java-rough-guide-to-character-encoding.html - Mark Jeronimus
1
在 @AllenGeorge 的评论后,我非常兴奋地想阅读这篇文章,但却发现链接已经失效了 :( 这该死的 Oracle,他们无法正确地进行 301 重定向。有人能够更新一下这个链接吗? - dimo414
我尝试在Oracle网站上搜索并找到了这个链接:http://www.oracle.com/us/technologies/java/supplementary-142654.html。 - Wittaya

14

Java使用UTF-16编码。一个Java char 只能表示基本多文种平面的字符。其他字符需要用由两个char组成的代理对来表示。这可以通过API方法如String.codePointAt()来反映。

是的,这意味着在处理超出基本多文种平面的字符时,很多Java代码会以某种方式出现问题。


2
String.lengthsubstring等如何处理包含这些字符的字符串? - Bart van Heukelom
6
@Bart: length()函数会把这样的字符算成两个字符,substring()函数也是如此,并且会愉快地将它们分开,导致无效的UTF-16编码。这是因为这样的字符只在Java设计之后成为Unicode的一部分,而Java不进行破坏性的改变。因此,新的方法被添加以处理代理对,但旧方法保持不变。 - Michael Borgwardt

13

除了其他回答中提到的内容外,还有一些需要记住的要点:

  • Java中的char类型始终占用16位

  • Unicode字符在UTF-16编码时,"几乎总是"(不总是)占用16位:这是因为有超过64K个Unicode字符。因此,Java char不是Unicode字符(尽管"几乎总是"是)。

  • 上面的"几乎总是"指的是Unicode的前64K个代码点,范围为0x0000到0xFFFF(BMP),在UTF-16编码中占用16位。

  • 非BMP("稀有")Unicode字符表示为两个Java chars(代理表示)。这也适用于作为字符串的文字表示形式:例如,字符U+20000写为"\uD840\uDC00"。

  • 推论:string.length()返回的是Java chars的数量,而不是Unicode chars的数量。一个只包含一个"稀有"Unicode字符(例如U+20000)的字符串将返回length() = 2。对于处理char序列的任何方法都适用相同的考虑。

  • Java对于整个非BMP Unicode字符的处理能力很弱。有一些实用程序方法将字符作为代码点处理,表示为int,例如:Character.isLetter(int ch)。这些是真正的完全Unicode方法。


你是不是指的是0x0000到0xFFFF?你只写了3个F。 - JoelFan
“几乎总是”?Unicode 中超过半数的字符都定义在 64K 边界之上,Unicode 12.1 定义了 137994 个字符,并被 Java 14 支持。 - Basil Bourque

6

您说:

Java的char类型占用2个字节(最大大小为65,536),但Unicode字符有95,221个。

Unicode扩展

实际上,Unicode中定义的字符清单已经大幅增长。Unicode继续增长——不仅仅是因为表情符号

  • Unicode 13有143,859个字符(Java 15,发布说明
  • Unicode 12.1有137,994个字符(Java 13和14)
  • Unicode 10有136,755个字符(Java 11和12)
  • Unicode 8有120,737个字符(Java 9)
  • Unicode 6.2有110,182个字符(Java 8)
  • Unicode 6.0有109,449个字符(Java 7)
  • Unicode 4.0有96,447个字符(Java 5和6)
  • Unicode 3.0有49,259个字符(Java 1.4)
  • Unicode 2.1有38,952个字符(Java 1.1.7)
  • Unicode 2.0有38,950个字符(Java 1.1)
  • Unicode 1.1.5有34,233个字符(Java 1.0)

char是遗留的

char类型已经过时,现在已被遗弃

使用码点数

相反,您应该使用码点数字。


你问:

这意味着Java应用程序无法处理某些Unicode字符吗?

char类型只能表示不到今天 Unicode 字符的一半。

要表示任何 Unicode 字符,请使用代码点数字。 永远不要使用 char

Unicode 中的每个字符都分配了一个代码点号。 这些数字超过一百万,从0到1,114,112。 根据上面列出的数字进行计算,这意味着该范围内的大多数数字尚未分配给字符。 其中一些数字被保留为 私有使用区 ,并永远不会被分配。

String类已经增加了处理代码点数字的方法,Character类也是如此。

通过零基索引号获取字符串中任意字符的代码点数。在这里,我们获取字母a的代码点数为97

int codePoint = "Cat".codePointAt( 1 ) ; // 97 = 'a', hex U+0061, LATIN SMALL LETTER A.

如果要使用更通用的CharSequence而不是String,请使用{{link1:Character.codePointAt}}。

我们可以获取代码点号对应的Unicode名称。

String name = Character.getName( 97 ) ; // letter `a`

小写拉丁字母 A

我们可以获得一个字符串中所有字符的码位数流。

IntStream codePointsStream = "Cat".codePoints() ;

我们可以将其转换为一个整数对象的列表。请参见如何将Java 8 IntStream转换为List?
List< Integer > codePointsList = codePointsStream.boxed().collect( Collectors.toList() ) ;

任何代码点号都可以通过调用Character.toString转换为单个字符的String
String s = Character.toString( 97 ) ; // 97 is `a`, LATIN SMALL LETTER A. 

a

我们可以从一个代码点数的IntStream生成一个String对象。请参见如何从代码点数的IntStream中制作字符串?
IntStream intStream = IntStream.of( 67 , 97 , 116 , 32 , 128_008 ); // 32 = SPACE, 128,008 = CAT (emoji).

String output =
        intStream
                .collect(                                     // Collect the results of processing each code point.
                        StringBuilder :: new ,                // Supplier<R> supplier
                        StringBuilder :: appendCodePoint ,    // ObjIntConsumer<R> accumulator
                        StringBuilder :: append               // BiConsumer<R,​R> combiner
                )                                             // Returns a `CharSequence` object.
                .toString();                                  // If you would rather have a `String` than `CharSequence`, call `toString`. 


您问道:

这是否归结于您使用的字符编码?

在Java中,String 内部始终使用UTF-16

只有在将文本导入或导出到Java字符串时才会使用其他字符编码。

因此,回答您的问题,字符编码与此无直接关系。一旦您将文本输入Java String 中,它就是以UTF-16编码存在的,因此可以包含任何Unicode字符。当然,要看到该字符,您必须使用具有特定字符字形定义的字体。

当从Java字符串导出文本时,如果您指定了一个不能表示文本中使用的某些Unicode字符的传统字符编码,则会遇到问题。因此,请使用现代字符编码,现在这意味着UTF-8,因为UTF-16现在被视为有害

3
请查看Unicode 4.0在J2SE 1.5中的支持文章,了解Sun发明的技巧以提供对所有Unicode 4.0代码点的支持。
总之,在Java 1.5中,您将找到以下有关Unicode 4.0的更改:
- char是UTF-16代码单元,而不是代码点 - 新的低级API使用int来表示Unicode代码点 - 高级API已更新以理解代理对 - 偏向于char序列API而非基于char的方法
由于Java没有32位字符,因此我让您判断是否可以称其为良好的Unicode支持。

“Unicode支持”可以通过多种方式实现,包括(但不限于)UTF-8、UTF-16和UTF-32编码。在各种编码之间需要考虑权衡,但选择UTF-16而不是UTF-32并没有什么“不好”的地方。 - DevSolar

3
这里是Oracle关于Unicode字符表示的文档。或者,如果您愿意,可以在这里找到更详细的文档
char数据类型(因此Character对象所封装的值)基于最初的Unicode规范,该规范将字符定义为固定宽度的16位实体。 Unicode标准已经更改,以允许其表示需要超过16位的字符。合法代码点的范围现在是U+0000到U+10FFFF,称为Unicode标量值(请参阅Unicode标准中U+n符号的定义)。从U+0000到U+FFFF的字符集有时称为基本多语言平面(BMP)。代码点大于U+FFFF的字符称为补充字符。Java 2平台在char数组和String和StringBuffer类中使用UTF-16表示法。在这种表示中,补充字符表示为一对char值,第一个值来自高代理项范围(\uD800-\uDBFF),第二个值来自低代理项范围(\uDC00-\uDFFF)。因此,char值表示基本多语言平面(BMP)代码点,包括替代码点或UTF-16编码的代码单元。int值表示所有Unicode代码点,包括补充代码点。int的低(最低有效)21位用于表示Unicode代码点,上(最高有效)11位必须为零。除非另有说明,否则与补充字符和替代char值相关的行为如下:只接受char值的方法无法支持补充字符。它们将代理范围的char值视为未定义字符。例如,Character.isLetter('\uD840')返回false,即使在字符串中后跟任何低代理值,此特定值也表示字母。接受int值的方法支持所有Unicode字符,包括补充字符。例如,Character.isLetter(0x2F81A)返回true,因为代码点值表示一个字母(CJK表意文字)。

1

来自OpenJDK7 String文档

String表示UTF-16格式的字符串,其中补充字符由代理对表示(有关更多信息,请参见Character类中的Unicode字符表示部分)。索引值是char代码单元引用,因此补充字符在String中使用两个位置。


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