读书人

Java 平台中市的增补字符

发布时间: 2012-08-27 21:21:57 作者: rapoo

Java 平台中的增补字符

Java 平台中市的增补字符Java 平台中市的增补字符Java 平台中市的增补字符UTF-32 代码单元00000041000000DF0000677100010400UTF-16 代码单元004100DF6771D801DC00UTF-8 代码单元41C39FE69DB1F0909080

另外,本文在许多地方使用术语字符序列或 char 序列概括 Java 2 平台识别的所有字符序列的容器:char[],java.lang.CharSequence 的实现(例如 String 类),和 java.text.CharacterIterator 的实现。

这么多术语。它们与在 Java 平台中支持增补字符有什么关系呢?

Java 平台中增补字符的设计方法

JSR-204 专家组必须作出的主要决定是如何在 Java API 中表示增补字符,包括单个字符和所有形式的字符序列。专家组考虑并排除了多种方法:

重新定义基本类型 char,使其具有 32 位,这样也会使所有形式的 char 序列成为 UTF-32 序列。在现有的 16 位类型 char 的基础上,为字符引入一种新的 32 位基本类型(例如,char32)。所有形式的 Char 序列均基于 UTF-16。在现有的 16 位类型 char 的基础上,为字符引入一种新的 32 位基本类型(例如,char32)。StringStringBuffer 接受并行 API,并将它们解释为 UTF-16 序列或 UTF-32 序列;其他 char 序列继续基于 UTF-16。使用 int 表示增补的代码点。StringStringBuffer 接受并行 API,并将它们解释为 UTF-16 序列或 UTF-32 序列;其他char 序列继续基于 UTF-16。使用代理 char 对,表示增补代码点。所有形式的 char 序列基于 UTF-16。引入一种封装字符的类。StringStringBuffer 接受新的 API,并将它们解释为此类字符的序列。使用一个 CharSequence 实例和一个索引的组合表示代码点。

在这些方法中,一些在早期就被排除了。例如,重新定义基本类型 char,使其具有 32 位,这对于全新的平台可能会非常有吸引力,但是,对于 J2SE 来说,它会与现有的 Java 虚拟机1、序列化和其他接口不兼容,更不用说基于 UTF-32 的字符串要使用两倍于基于 UTF-16 的字符串的内存了。添加一种新类型的 char32 可能会简单一些,但是仍然会出现虚拟机和序列化方面的问题。而且,语言更改通常需要比 API 更改有更长的提前期,因此,前面两种方法会对增补字符支持带来无法接受的延迟。为了在余下的方法中筛选出最优方案,实现小组使用四种不同的方法,在大量进行低层字符处理的代码(java.util.regex 包)中实现了对增补字符支持,并对这四种方法的难易程度和运行表现进行了比较。

最终,专家组确定了一种分层的方法:

使用基本类型 int 在低层 API 中表示代码点,例如 Character 类的静态方法。将所有形式的 char 序列均解释为 UTF-16 序列,并促进其在更高层级 API 中的使用。提供 API,以方便在各种 char 和基于代码点的表示法之间的转换。

在需要时,此方法既能够提供一种概念简明且高效的单个字符表示法,又能够充分利用通过改进可支持增补字符的现有 API。同时,还能够促进字符序列在单个字符上的应用,这一点一般对于国际化的软件很有好处。

在这种方法中,一个 char 表示一个 UTF-16 代码单元,这样对于表示代码点有时并不够用。您会注意到,J2SE 技术规范现在使用术语代码点和 UTF-16 代码单元(表示法是相关的)以及通用术语字符(表示法与该讨论没有关系)。API 通常使用名称codePoint 描述表示代码点的类型 int 的变量,而 UTF-16 代码单元的类型当然为 char

我们将在下面两部分中了解到 J2SE 平台的实质变化 — 其中一部分介绍单个代码点的低层 API,另一部分介绍采用字符序列的高层接口。

开放的增补字符:基于代码点的 API

新增的低层 API 分为两大类:用于各种 char 和基于代码点的表示法之间转换的方法和用于分析和映射代码点的方法。

最基本的转换方法是 Character.toCodePoint(charhigh, charlow)(用于将两个 UTF-16 代码单元转换为一个代码点)和Character.toChars(intcodePoint)(用于将指定的代码点转换为一个或两个 UTF-16 代码单元,然后封装到一个char[] 内。不过,由于大多数情况下文本以字符序列的形式出现,因此,另外提供 codePointAtcodePointBefore 方法,用于将代码点从各种字符序列表示法中提取出来:Character.codePointAt(char[]a, intindex)String.codePointBefore(int index) 是两种典型的例子。在将代码点插入字符序列时,大多数情况下均有一些针对StringBufferStringBuilder 类的 appendCodePoint(int codePoint) 方法,以及一个用于提取表示代码点的int[]String 构建器。

几种用于分析代码单元和代码点的方法有助于转换过程:Character 类中的 isHighSurrogateisLowSurrogate 方法可以识别用于表示增补字符的char 值;charCount(int codePoint) 方法可以确定是否需要将某个代码点转换为一个或两个char

但是,大多数基于代码点的方法均能够对所有 Unicode 字符实现基于 char 的旧方法对 BMP 字符所实现的功能。以下是一些典型例子:

Character.isLetter(int codePoint) 可根据 Unicode 标准识别字母。Character.isJavaIdentifierStart(int codePoint) 可根据 Java 语言规范确定代码点是否可以启动标识符。Character.UnicodeBlock.of(int codePoint) 可搜索代码点所属的 Unicode 字符子集。Character.toUpperCase(int codePoint) 可将给定的代码点转换为其大写等值字符。尽管此方法能够支持增补字符,但是它仍然不能解决根本的问题,即在某些情况下,逐个字符的转换无法正确完成。例如,德文字符“"?"”应该转换为“SS”,这需要使用String.toUpperCase 方法。

注意大多数接受代码点的方法并不检查给定的 int 值是否处于有效的 Unicode 代码点范围之内(如上所述,只有 0x0 至 0x10FFFF 之间的范围是有效的)。在大多数情况下,该值是以确保其有效的方法产生的,在这些低层 API 中反复检查其有效性可能会对系统性能造成负面的影响。在无法确保有效性的情况下,应用程序必须使用Character.isValidCodePoint 方法确保代码点有效。大多数方法对于无效的代码点采取的行为没有特别加以指定,不同的实现可能会有所不同。

API 包含许多简便的方法,这些方法可使用其他低层的 API 实现,但是专家组觉得,这些方法很常用,将它们添加到 J2SE 平台上很有意义。不过,专家组也排除了一些建议的简便方法,这给我们提供了一次展示自己实现此类方法能力的机会。例如,专家组经过讨论,排除了一种针对String 类的新构建器(该构建器可以创建一个保持单个代码点的 String)。以下是使应用程序使用现有的 API 提供功能的一种简便方法:

/** * 创建仅含有指定代码点的新 String。 */String newString(int codePoint) {    return new String(Character.toChars(codePoint));}

您会注意到,在这个简单的实现中,toChars 方法始终创建一个中间数列,该数列仅使用一次即立即丢弃。如果该方法在您的性能评估中出现,您可能会希望将其优化为针对最为普通的情况,即该代码点为 BMP 字符:

/** * 创建仅含有指定代码点的新 String。 * 针对 BMP 字符优化的版本。 */String newString(int codePoint) {    if (Character.charCount(codePoint) == 1) {        return String.valueOf((char) codePoint);    } else {        return new String(Character.toChars(codePoint));    }}

或者,如果您需要创建许多个这样的 string,则可能希望编写一个重复使用 toChars 方法所使用的数列的通用版本:

/** * 创建每一个均含有一个指定 * 代码点的新 String。 * 针对 BMP 字符优化的版本。 */String[] newStrings(int[] codePoints) {    String[] result = new String[codePoints.length];    char[] codeUnits = new char[2];    for (int i = 0; i < codePoints.length; i++) {       int count = Character.toChars(codePoints[i], codeUnits, 0);       result[i] = new String(codeUnits, 0, count);    }    return result;}

不过,最终您可能会发现,您需要的是一个完全不同的解决方案。新的构建器 String(int codePoint) 实际上建议作为 String.valueOf(char) 的一个基于代码点的备选方案。在很多情况下,此方法用于消息生成的环境,例如:

System.out.println("Character " + String.valueOf(char) + " is invalid.");

新的格式化 API 支持增补文字,提供一种更加简单的备选方案:

System.out.printf("Character %c is invalid.%n", codePoint);

使用此高层 API 不仅简捷,而它有很多特殊的优点:它可以避免串联(串联会使消息很难本地化),并将需要移进资源包 (resource bundle) 的字符串数量从两个减少到一个。

增补字符透视:功能增强

在支持使用增补字符的 Java 2 平台中的大部分更改没有反映到新的 API 内。一般预期是,处理字符序列的所有接口将以适合其功能的方式处理增补字符。本部分着重讲述为达到此预期所作一些功能增强。

Java 编程语言中的标识符

Java 语言规范指出所有 Unicode 字母和数字均可用于标识符。许多增补字符是字母或数字,因此 Java 语言规范已经参照新的基于代码点的方法进行更新,以在标识符内定义合法字符。为使用这些新方法,需要检测标识符的 javac 编译器和其他工具都进行了修订。

库内的增补字符支持

许多 J2SE 库已经过增强,可以通过现有接口支持增补字符。以下是一些例子:

字符串大小写转换功能已更新,可以处理增补字符,也可以实现 Unicode 标准中规定的特殊大小写规则。java.util.regex 包已更新,这样模式字符串和目标字符串均可以包含增补字符并将其作为完整单元处理。现在,在 java.text 包内进行整理处理时,会将增补字符看作完整单元。java.text.Bidi 类已更新,可以处理增补字符和 Unicode 4.0 中新增的其他字符。请注意,Cypriot Syllabary 字符子集内的增补字符具有从右至左的方向性。Java 2D API 内的字体渲染和打印技术已经过增强,可以正确渲染和测量包含增补字符的字符串。Swing 文本组件实现已更新,可以处理包含增补字符的文本。字符转换

只有很少的字符编码可以表示增补字符。如果是基于 Unicode 的编码(如 UTF-8 和 UTF-16LE),则旧版的 J2RE 内的字符转换器已经按照正确处理增补字符的方式实现转换。对于 J2RE 5.0,可以表示增补字符的其他编码的转换器已更新:GB18030、x-EUC-TW(现在实现所有 CNS 11643 层面)和 Big5-HKSCS(现在实现 HKSCS-2001)。

在源文件内表示增补字符

在 Java 编程语言源文件中,如果使用可以直接表示增补字符的字符编码,则使用增补字符最为方便。UTF-8 是最佳的选择。在所使用的字符编码无法直接表示字符的情况下,Java 编程语言提供一种 Unicode 转义符语法。此语法没有经过增强,无法直接表示增补字符。而是使用两个连续的 Unicode 转义符将其表示为 UTF-16 字符表示法中的两个编码单元。例如,字符 U+20000 写作“\uD840\uDC00”。您也许不愿意探究这些转义序列的含义;最好是写入支持所需增补字符的编码,然后使用一种工具(如 native2ascii)将其转换为转义序列。

遗憾的是,由于其编码问题,属性文件仍局限于 ISO 8859-1(除非您的应用程序使用新的 XML 格式)。这意味着您始终必须对增补字符使用转义序列,而且可能要使用不同的编码进行编写,然后使用诸如 native2ascii 的工具进行转换。

经修订的 UTF-8

Java 平台对经修订的 UTF-8 已经很熟悉,但是,问题是应用程序开发人员在可能包含增补字符的文本和 UTF-8 之间进行转换时需要更加留神。需要特别注意的是,某些 J2SE 接口使用的编码与 UTF-8 相似但与其并不兼容。以前,此编码有时被称为“Java modified UTF-8”(经 Java 修订的 UTF-8) 或(错误地)直接称为“UTF-8”。对于 J2SE 5.0,其说明文档正在更新,此编码将统称为“modified UTF-8”(经修订的 UTF-8)。

经修订的 UTF-8 和标准 UTF-8 之间之所以不兼容,其原因有两点。其一,经修订的 UTF-8 将字符 U+0000 表示为双字节序列 0xC0 0x80,而标准 UTF-8 使用单字节值 0x0。其二,经修订的 UTF-8 通过对其 UTF-16 表示法的两个代理代码单元单独进行编码表示增补字符 。每个代理代码单元由三个字节来表示,共有六个字节。而标准 UTF-8 使用单个四字节序列表示整个字符。

Java 虚拟机及其附带的接口(如 Java 本机接口、多种工具接口或 Java 类文件)在 java.io.DataInputDataOutput 接口和类中使用经修订的 UTF-8 实现或使用这些接口和类 ,并进行序列化。Java 本机接口提供与经修订的 UTF-8 之间进行转换的例程。而标准 UTF-8 由String 类、java.io.InputStreamReaderOutputStreamWriter 类、java.nio.charset 设施 (facility) 以及许多其上层的 API 提供支持。

由于经修订的 UTF-8 与标准的 UTF-8 不兼容,因此切勿同时使用这两种版本的编码。经修订的 UTF-8 只能与上述的 Java 接口配合使用。在任何其他情况下,尤其对于可能来自非基于 Java 平台的软件的或可能通过其编译的数据流,必须使用标准的 UTF-8。需要使用标准的 UTF-8 时,则不能使用 Java 本机接口例程与经修订的 UTF-8 进行转换。

在应用程序内支持增补字符

现在,对大多数读者来说最为重要的问题是:必须对应用程序进行哪些更改才能支持增补字符?

答案取决于在应用程序中进行哪种类型的文本处理和使用哪些 Java 平台 API。

对于仅以各种形式 char 序列([char[]java.lang.CharSequence 实现、java.text.CharacterIterator 实现)处理文本和仅使用接受和退回序列(如char 序列)的 Java API 的应用程序,可能根本不需要进行任何更改。Java 平台 API 的实现应该能够处理增补字符。

对于本身解释单个字符、将单个字符传送给 Java 平台 API 或调用能够返回单个字符的方法的应用程序,则需要考虑这些字符的有效值。在很多情况下,往往不要求支持增补字符。例如,如果某应用程序搜索char 序列中的 HTML 标记,并逐一检查每个 char,它会知道这些标记仅使用 Basic Latin 字符子集中的字符。如果所搜索的文本含有增补字符,则这些字符不会与标记字符混淆,因为 UTF-16 使用代码单元表示增补字符,而代码单元的值不会用于 BMP 字符。

只有在某应用程序本身解释单个字符、将单个字符传送给 Java 平台 API 或调用能够返回单个字符的方法且这些字符可能为增补字符时,才必须更改该应用程序。在提供使用char 序列的并行 API 时,最好转而使用此类 API。在其他情况下,有必要使用新的 API 在 char 和基于代码点的表示法之间进行转换,并调用基于代码点的 API。当然,如果您发现在 J2SE 5.0 中有更新、更方便的 API,使您能够支持增补字符并同时简化代码(如上格式化范例 中所述),则没有必要这样做。

您可能会犹豫,是将所有文本转换为代码点表示法(即 int[])然后在该表示法中处理,还是在大多数情况下仍采用 char 序列,仅在需要时转换为代码点,两者之间孰优孰劣很难确定。当然,总体来说,Java 平台 API 相对于char 序列肯定具有一定的优势,而且采用 Java 平台 API 可以节省内存空间。

对于需要与 UTF-8 之间进行转换的应用程序,还需要认真考虑是需要标准的 UTF-8 还是经修订的 UTF-8,并针对每种 UTF-8 采用适当的 Java 平台。“经修订的 UTF-8”部分介绍进行正确选择所需的信息。

使用增补字符测试应用程序

经过前面部分的介绍后,无论您是否需要修订应用程序,测试应用程序是否运行正常始终是一种正确的做法。对于不含有图形用户界面的应用程序,有关“在源文件内表示增补字符” 的信息有助于设计测试用例。以下是有关使用图形用户界面进行测试的补充信息。

对于文本输入,Java 2 SDK 提供用于接受“\Uxxxxxx”格式字符串的代码点输入方法,这里大写的“U”表示转义序列包含六个十六进制数字,因此允许使用增补字符。小写的“u”表示转义序列“\uxxxx”的原始格式。您可以在 J2SDK 目录 demo/jfc/CodePointIM 内找到此输入方法及其说明文档。

对于字体渲染,您需要至少能够渲染一些增补字符的字体。其中一种此类字体为 James Kass 的 Code2001 字体,它提供手写体字形(如 Deseret 和 Old Italic)。利用 Java 2D 库中提供新功能,您只需将该字体安装到 J2RE 的 lib/fonts/fallback 目录内即可,然后它可自动添加至在 2D 和 XAWT 渲染时使用的所有逻辑字体 — 无需编辑字体配置文件。

至此,您就可以确认,您的应用程序能够完全支持增补字符了!

结论

对增补字符的支持已经引入 Java 平台,大部分应用程序无需更改代码即可处理这些字符。解释单个字符的应用程序可以在 Character 类和多种CharSequence 子类中使用基于代码点的新 API。

鸣谢

Java 平台中的增补字符支持由 Java Community Process 的 JSR-204 专家组设计。技术规范设计主持为 Masayoshi Okutsu 和 Brian Beck (Sun Microsystems),其他专家组成员有 Craig Cummings (Oracle)、Mark Davis (IBM)、Markus Eble (SAP AG)、Jere K?pyaho (Nokia Corp.)、Kazuhiro Kazama (NTT)、Kenji Kazumura (Fujitsu Limited)、Eiichi Kimura (NEC Corp.)、Changshin Lee (Tmax Soft Inc.) 和 Toshiki Murata (Oki Electric Industry Co.)。参考实现由 Sun Microsystems 的 Java Internationalization 团队完成,并承蒙位于圣何塞的 IBM Globalization Center of Competency 的协助。技术规范的技术兼容套件为 Java Compatibility Kit,由 Sun Microsystems 的 JCK 团队实现。

参考书目

Masayoshi Okutsu, Brian Beck (ed.): UnicodeSupplementary Character Support. Proposed Final Draft. Sun Microsystems, 2004.

Java2 Platform Standard Edition 5.0 API Specification. Sun Microsystems, 2004.

The Unicode Consortium: The Unicode Standard, Version 4.0. Addison-Wesley, 2003.

Ken Whistler, Mark Davis: Character Encoding Model. Unicode Technical Report #17. The Unicode Consortium, 2000.

James Kass: Code2001, a Plane 1 Unicode-based Font.

关于作者

Norbert Lindenberg 是 Sun Microsystems 的 Java Web Services 团队内 Java Internationalization 技术主管。在加盟 Sun 之前,曾经供职于 General Magic 和 Apple Computer,参与过多个国际化项目。他毕业于德国的卡尔斯鲁厄大学,拥有计算机科学理科硕士学位。

Masayoshi Okutsu 是 Sun Microsystems 的 Java Web Services 团队的一名国际化工程师,目前担任 Unicode Supplementary Character Support 的 Java Specification Request 204 的技术规范主管。在加盟 Sun Microsystems 之前,供职于 Digital Equipment Corporation,期间曾经参与多个国际化项目。他毕业于日本山形大学,拥有电子工程理学士学位。

?


1 本网站中使用的术语“Java 虚拟机”或“JVM”是指针对 Java 平台的虚拟机。

读书人网 >编程

热点推荐