Java中的代码点和代码单元

摘要

    本文介绍 Java 平台支持增补字符的方式。增补字符是 Unicode 标准中代码点超出 U+FFFF 的字符,因此它们无法在Java 编程语言中描述为单个的 16 位实体(例如char数据类型)。这些字符一般极少用,但是,有些会在诸如中文或日文人名中用到,因此,在东亚国家,政府应用程序通常会要求支持这些字符。

    Java 平台目前正在改进,以便支持对增补字符的处理,这种改进对现有的应用程序影响微乎其微。新的低层 API 在需要时能够使用单个的字符运行。不过,大多数文本处理 API 均使用字符序列,例如String类或字符数组。现在,这些均解释为 UTF-16 序列,而且,这些 API 实现已转变为正确地处理增补字符。这些改进已融入 Java 2 平台 5.0 版,标准版 (J2SE)。

    除详细解释这些改进之外,本文同时为应用程序开发人员确定和实现必要的更改提供指导,以支持整个 Unicode 字符集的使用。

    背景

    Unicode 最初设计是作为一种固定宽度的 16 位字符编码。在 Java 编程语言中,基本数据类型char初衷是通过提供一种简单的、能够包含任何字符的数据类型来充分利用这种设计的优点。不过,现在看来,16 位编码的所有 65,536 个字符并不能完全表示全世界所有正在使用或曾经使用的字符。于是,Unicode 标准已扩展到包含多达 1,112,064 个字符。那些超出原来的 16 位限制的字符被称作增补字符。Unicode 标准 2.0 版是第一个包含启用增补字符设计的版本,但是,直到 3.1 版才收入第一批增补字符集。由于 J2SE 的 5.0 版必须支持 Unicode 标准 4.0 版,因此它必须支持增补字符。

    对增补字符的支持也可能会成为东亚市场的一个普遍商业要求。政府应用程序会需要这些增补字符,以正确表示一些包含罕见中文字符的姓名。出版应用程序可能会需要这些增补字符,以表示所有的古代字符和变体字符。中国政府要求支持 GB18030(一种对整个 Unicode 字符集进行编码的字符编码标准),因此,如果是 Unicode 3.1 版或更新版本,则将包括增补字符。台湾标准 CNS-11643 包含的许多字符在 Unicode 3.1 中列为增补字符。香港政府定义了一种针对粤语的字符集,其中的一些字符是 Unicode 中的增补字符。最后,日本的一些供应商正计划利用增补字符空间中大量的专用空间收入 50,000 多个日文汉字字符变体,以便从其专有系统迁移至基于 Java 平台的解决方案。

    因此,Java 平台不仅需要支持增补字符,而且必须使应用程序能够方便地做到这一点。由于增补字符打破了 Java 编程语言的基础设计构想,而且可能要求对编程模型进行根本性的修改,因此,Java Community Process 召集了一个专家组,以期找到一个适当的解决方案。该小组被称为 JSR-204 专家组,使用Unicode 增补字符支持的 Java 技术规范请求的编号。从技术上来说,该专家组的决定仅适用于 J2SE 平台,但是由于 Java 2 平台企业版 (J2EE) 处于 J2SE 平台的最上层,因此它可以直接受益,我们期望 Java 2 平台袖珍版 (J2ME) 的配置也采用相同的设计方法。

    不过,在了解 JSR-204 专家组确定的解决方案之前,我们需要先理解一些术语。

    代码点、字符编码方案、UTF-16:这些是指什么?

    不幸的是,引入增补字符使字符模型变得更加复杂了。在过去,我们可以简单地说“字符”,在一个基于 Unicode 的环境(例如 Java 平台)中,假定字符有 16 位,而现在我们需要更多的术语。我们会尽量介绍得相对简单一些 — 如需了解所有详细的讨论信息,您可以阅读Unicode 标准第 2 章或 Unicode 技术报告 17“字符编码模型”。Unicode 专业人士可略过所有介绍直接参阅本部分中的最后定义。

    字符是抽象的最小文本单位。它没有固定的形状(可能是一个字形),而且没有值。“A”是一个字符,“€”(德国、法国和许多其他欧洲国家通用货币的标志)也是一个字符。

    字符集是字符的集合。例如,汉字字符是中国人最先发明的字符,在中文、日文、韩文和越南文的书写中使用。

    编码字符集是一个字符集,它为每一个字符分配一个唯一数字。Unicode 标准的核心是一个编码字符集,字母“A”的编码为 004116 和字符“€”的编码为20AC16.Unicode 标准始终使用十六进制数字,而且在书写时在前面加上前缀“U+”,所以“A”的编码书写为“U+0041”。

    代码点是指可用于编码字符集的数字。编码字符集定义一个有效的代码点范围,但是并不一定将字符分配给所有这些代码点。有效的 Unicode 代码点范围是 U+0000 至 U+10FFFF.Unicode 4.0 将字符分配给一百多万个代码点中的 96,382 代码点。

    增补字符是代码点在 U+10000 至 U+10FFFF 范围之间的字符,也就是那些使用原始的 Unicode 的 16 位设计无法表示的字符。从 U+0000 至 U+FFFF 之间的字符集有时候被称为基本多语言面 (BMP)。因此,每一个 Unicode 字符要么属于 BMP,要么属于增补字符。

    字符编码方案是从一个或多个编码字符集到一个或多个固定宽度代码单元序列的映射。最常用的代码单元是字节,但是 16 位或 32 位整数也可用于内部处理。UTF-32、UTF-16 和 UTF-8 是 Unicode 标准的编码字符集的字符编码方案。

    UTF-32 即将每一个 Unicode 代码点表示为相同值的 32 位整数。很明显,它是内部处理最方便的表达方式,但是,如果作为一般字符串表达方式,则要消耗更多的内存。

    UTF-16 使用一个或两个未分配的 16 位代码单元的序列对 Unicode 代码点进行编码。值 U+0000 至 U+FFFF 编码为一个相同值的 16 位单元。增补字符编码为两个代码单元,第一个单元来自于高代理范围(U+D800 至 U+DBFF),第二个单元来自于低代理范围(U+DC00 至 U+DFFF)。这在概念上可能看起来类似于多字节编码,但是其中有一个重要区别:值 U+D800 至 U+DFFF 保留用于 UTF-16;没有这些值分配字符作为代码点。这意味着,对于一个字符串中的每个单独的代码单元,软件可以识别是否该代码单元表示某个单单元字符,或者是否该代码单元是某个双单元字符的第一个或第二单元。这相当于某些传统的多字节字符编码来说是一个显著的改进,在传统的多字节字符编码中,字节值 0x41 既可能表示字母“A”,也可能是一个双字节字符的第二个字节。

    UTF-8 使用一至四个字节的序列对编码 Unicode 代码点进行编码。U+0000 至 U+007F 使用一个字节编码,U+0080 至 U+07FF 使用两个字节,U+0800 至 U+FFFF 使用三个字节,而 U+10000 至 U+10FFFF 使用四个字节。UTF-8 设计原理为:字节值 0x00 至 0x7F 始终表示代码点 U+0000 至 U+007F(Basic Latin 字符子集,它对应 ASCII 字符集)。这些字节值永远不会表示其他代码点,这一特性使 UTF-8 可以很方便地在软件中将特殊的含义赋予某些 ASCII 字符。

    下表所示为几个字符不同表达方式的比较:

Unicode
U+0041
U+00DF
U+6771
U+10400
表示字形
UTF-32 码单
00000041
000000DF
00006771
00010400
UTF-16 码单
0041
00DF
6771
D801
DC00
UTF-8 码单
41
C3
9F
E6
9D
B1
F0
90
90
80

    另外,本文在许多地方使用术语字符序列或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)。String和StringBuffer接受并行 API,并将它们解释为 UTF-16 序列或 UTF-32 序列;其他char序列继续基于 UTF-16.
        使用int表示增补的代码点。String和StringBuffer接受并行 API,并将它们解释为 UTF-16 序列或 UTF-32 序列;其他char序列继续基于 UTF-16.
        使用代理char对,表示增补代码点。所有形式的char序列基于 UTF-16.
        引入一种封装字符的类。String和StringBuffer接受新的 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(char high, char low)(用于将两个 UTF-16 代码单元转换为一个代码点)和Character.toChars(int codePoint)(用于将指定的代码点转换为一个或两个 UTF-16 代码单元,然后封装到一个char[]内。不过,由于大多数情况下文本以字符序列的形式出现,因此,另外提供codePointAt和codePointBefore方法,用于将代码点从各种字符序列表示法中提取出来:Character.codePointAt(char[] a, int index)和String.codePointBefore(int index)是两种典型的例子。在将代码点插入字符序列时,大多数情况下均有一些针对StringBuffer和StringBuilder类的appendCodePoint(int codePoint)方法,以及一个用于提取表示代码点的int[]的String构建器。

    几种用于分析代码单元和代码点的方法有助于转换过程:Character 类中的isHighSurrogate和isLowSurrogate方法可以识别用于表示增补字符的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.DataInput和DataOutput接口和类中使用经修订的 UTF-8 实现或使用这些接口和类,并进行序列化。Java 本机接口提供与经修订的 UTF-8 之间进行转换的例程。而标准 UTF-8 由String类、java.io.InputStreamReader和OutputStreamWriter类、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.

    以下是Unicode和UTF-8之间的转换关系表:

U-00000000 - U-0000007F: 0xxxxxxx
U-00000080 - U-000007FF: 110xxxxx 10xxxxxx
U-00000800 - U-0000FFFF: 1110xxxx 10xxxxxx 10xxxxxx
U-00010000 - U-001FFFFF: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
U-00200000 - U-03FFFFFF: 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
U-04000000 - U-7FFFFFFF: 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 

    Byte 数组转整数:

static int bytes2int(byte[] b)
    { 
        int mask=0xff;
        int temp=0;
        int res=0;
        for(int i=0;i<4;i++){
             res<<=8;
             temp=b[i]&mask;
             res|=temp;
        } 
    return res;
}

整数转byte数组:

static byte[] int2bytes(int num)
    { 
        byte[] b=new byte[4];
        int mask=0xff;
        for(int i=0;i<4;i++){ 
            b[i]=(byte)(num>>>(24-i*8));
        } 
    return b;
}

 




在Java字符串处理时,在使用length和charAt方法时,应该格外小心,因为length返回的是UTF-16编码表示下的代码单元数量,而非我们所认为的字符的个数,charAt方法返回的是指定位置处的代码单元,而非我们所认为的字符。

至于为什么都是“代码单元”而非字符,这和Unicode字符集的增补相关,具体的参看下面的附录。

要想获得字符串中的字符的个数,应当使用aString.codePointCount(0, aString.length());要想获得指定位置处的字符,使用aString.codePointAt(i);需要注意codePointAt的返回值,是int而非char。

枚举字符串的正确方法:

 

for (int i = 0; i < aString.length();) {
int character = aString.codePointAt(i);
if (Character.isSupplementaryCodePoint(character)) i += 2;
else ++i;
}

将codePoint转换为char[]可调用Character.toChars方法,然后可进一步转换为字符串:

String s(Character.toChars(codePoint));

 

附录A:

《Java核心技术》中关于字符和字符串的讲解:

 

3.3.3 char类型

 

    char类型用于表示单个字符。通常用来表示字符常量。例如:'A'是编码为65所对应的字符常量。与"A"不同,"A"是一个包含字符A的字符串。Unicode编码单元可以表示为十六进制值,其范围从\u0000到\uffff。例如:。\u2122表示注册符号,\u03C0表示希腊字母π。

    除了可以采用转义序列符\u表示Unicode代码单元的编码之外,还有一些用于表示特殊字符的转义序列符,请参看表3-3。所有这些转义序列符都可以出现在字符常量或字符串的引号内。例如,'\u2122'或"Hello\n"。转义序列符\u还可以出现在字符常量或字符串的引号之外(而其他所有转义序列不可以)。例如:

    public static void main(String\u005B\u005D args)

    这种形式完全符合语法规则,\u005B和\u005D是[和]的编码。

表3-3 特殊字符的转义序列符

转义序列

名称

Unicode值

\b

退格

\u0008

\t

制表

\u0009

\n

换行

\u000a

\r

回车

\u000d

\"

双引号

\u0022

\'

单引号

\u0027

\\

反斜杠

\u005c

    要想弄清char类型,就必须了解Unicode编码表。Unicode打破了传统字符编码方法的限制。在Unicode出现之前,已经有许多种不同的标准:美国的ASCII、西欧语言中的ISO 8859-1、俄国的KOI-8、中国的GB118030和BIG-5等等,这样就产生了下面两个问题:一个是对于任意给定的代码值,在不同的编码方案下有可能对应不同的字母;二是采用大字符集的语言其编码长度有可能不同。例如,有些常用的字符采用单字节编码,而另一些字符则需要两个或更多个字节。

    设计Unicode编码的目的就是要解决这些问题。在20世纪80年代开始启动设计工作时,人们认为两个字节的代码宽度足以能够对世界上各种语言的所有字符进行编码,并有足够的空间留给未来的扩展。在1991年发布了Unicode 1.0,当时仅占用65 536个代码值中不到一半的部分。在设计Java时决定采用16位的Unicode字符集,这样会比使用8位字符集的程序设计语言有很大的改进。

    十分遗憾,经过一段时间,不可避免的事情发生了。Unicode字符超过了65 536个,其主要原因是增加了大量的汉语、日语和韩国语言中的表意文字。现在,16位的char类型已经不能满足描述所有Unicode字符的需要了。

    下面利用一些专用术语解释一下Java语言解决这个问题的基本方法。从JDK 5.0开始,代码点(code point)是指与一个编码表中的某个字符对应的代码值。在Unicode标准中,代码点采用十六进制书写,并加上前缀U+,例如U+0041就是字母A的代码点。Unicode代码点可以分成17个代码级别(code plane)。第一个代码级别称为基本的多语言组别(basic multilingual plane),代码点从U+0000到U+FFFF,其中包栝了经典的Unicode代码。其余的16个附加级别,代码点从U+10000到U+10FFFF,其中包栝了一些辅助字符(supplementarycharacter)。

    UTF-16编码采用不同长度的编码表示所有Unicode代码点。在基本的多语言级别中,每个字符用16位表示,通常被称为代码单元(code unit);而辅助字符采用对连续的代码单元进行编码。这样构成的编码值一定落入基本的多语言级别中空闲的2048字节内,通常被称为替代区域(surrogate area)[U+D800~U+DBFF用于第一个代码单元,U+DC00〜U+DFFF用于第二个代码单元]。这样设计十分巧妙,我们可以从中迅速地知道一个代码单元是一个字符的编码,还是一个辅助字符的第一或第二部分。例如,对于整数集合的数学符号,它的代码点是U+1D568,并且是用两个代码单元U+D835和U+DD68编码的(存关编码算法的描述请参看http://en.wikipe-dia.org/wiki/UTF-16)。

    在Java中,char类型用UTF-16编码描述一个代码单元。

    我们强烈建议不要在程序中使用char类型,除非确实需要对UTF-16代码单元进行操作。最好将需要处理的字符串用抽象数据类型表示(有关这方面的内容将在稍后讨论)。

 

3.6.6 代码点与代码单元

 

    Java字符串由char序列组成。从前面已经看到,字符数据类型是一个采用UTF-16编码表示Unicode代码点的代码单元。大多数的常用Unicode字符使用一个代码单元就可以表示,而辅助字符需要一对代码单元表示。

    length方法将返回采用UTF-16编码表示的给定字符串所需要的代码单元数量。例如:

        Stringgreeting = "Hello";

        int n = greeting.length();// is 5

    要想得到实际的长度,即代码点数量,可以调用:

        int cpCount =greeting.codePointCount(0, greeting.length());

    调用s.charAt(n)将返回位置n的代码单元,n介于0~s.length()-1之间。例如:

        char first =greeting.charAt(0); // first is 'H'

        char last =greeting.charAt(4); // last is 'o'

    要想得到第i个代码点,应该使用下列语句

        int index =greeting.offsetByCodePoints(0, i);

        int cp =greeting.codePointAt(index);

    注释:Java以独特的风格对字符串中的代码单元计数:字符串中的第一个代码单元位置为0。这种习愤起源于C,这样处理主要出于技术上的原因。具体理由似乎已经淡忘,而麻烦却保留了下来。但是,许多程序员习惯于这种风格,因而Java设计者也就将其保留了下来。

    为什么会对代码单元如此大惊小怪?请考虑下列语句:

    Ƶis the set of integers

    使用UTF-16编码表示Ƶ需要两个代码单元。调用char ch =sentence.charAt(1);返回的不是空格,而是第二个代码单元Z。为了避免这种情况的发生,请不要使用char类型。这太低级了。

    如果想要遍历一个字符串,并且依次査看每一个代码点,可以使用下列语句:

        int cp =sentence.codePointAt(i);

        if (Character.isSupplementaryCodePoint(cp))i += 2;

        else i++;

    非常幸运,codePointAt方法能够辨别一个代码单元是辅助字符的第一部分还是第二部分,并能够返回正确的结果。也就是说,可以使用下列语句实现回退操作:

        i--;

        int cp =sentence.codePointAt(i);

        if (Character.isSupplementaryCodePoint(cp))i--;

背景

    Unicode最初设计是作为一种固定宽度的16位字符编码。在Java编程语言中,基本数据类型char初衷是通过提供一种简单的、能够包含任何字符的数据类型来充分利用这种设计的优点。不过,现在看来,16位编码的所有65 536个字符并不能完全表示全世界所有正在使用或曾经使用的字符。于是,Unicode标准已扩展到包含多达1 112 064个字符。那些超出原来的16位限制的字符被称作增补字符。Unicode标准2.0版是第一个包含启用增补字符设计的版本,但是,直到3.1版才收入第一批增补字符集。由于J2SE的5.0版必须支持Unicode标准4.0版,因此它必须支持增补字符。

    对增补字符的支持也可能会成为东亚市场的一个普遍商业要求。政府应用程序会需要这些增补字符,以正确表示一些包含罕见中文字符的姓名。出版应用程序可能会需要这些增补字符,以表示所有的古代字符和变体字符。中国政府要求支持GB18030(一种对整个Unicode字符集进行编码的字符编码标准),因此,如果是Unicode 3.1版或更新版本,则将包括增补字符。台湾标准CNS-11643包含的许多字符在Unicode 3.1中列为增补字符。香港政府定义了一种针对粤语的字符集,其中的一些字符是Unicode中的增补字符。最后,日本的一些供应商正计划利用增补字符空间中大量的专用空间收入50 000多个日文汉字字符变体,以便从其专有系统迁移至基于Java平台的解决方案。

    因此,Java平台不仅需要支持增补字符,而且必须使应用程序能够方便地做到这一点。由于增补字符打破了Java编程语言的基础设计构想,而且可能要求对编程模型进行根本性的修改,因此,Java Community Process召集了一个专家组,以期找到一个适当的解决方案。该小组被称为JSR-204专家组,使用Unicode增补字符支持的Java技术规范请求的编号。从技术上来说,该专家组的决定仅适用于J2SE平台,但是由于Java2平台企业版(J2EE)处于J2SE平台的最上层,因此它可以直接受益,我们期望Java2平台袖珍版(J2ME)的配置也采用相同的设计方法。

    不过,在了解JSR-204专家组确定的解决方案之前,我们需要先理解一些术语。

代码点、字符编码方案、UTF-16:这些是指什么?

    不幸的是,引入增补字符使字符模型变得更加复杂了。在过去,我们可以简单地说“字符”,在一个基于Unicode的环境(例如Java平台)中,假定字符有16位,而现在我们需要更多的术语。我们会尽量介绍得相对简单一些—如需了解所有详细的讨论信息,您可以阅读Unicode标准第2章或Unicode技术报告17“字符编码模型”。Unicode专业人士可略过所有介绍直接参阅本部分中的最后定义。

    字符是抽象的最小文本单位。它没有固定的形状(可能是一个字形),而且没有值。“A”是一个字符,“€”(德国、法国和许多其他欧洲国家通用货币的标志)也是一个字符。

    字符集是字符的集合。例如,汉字字符是中国人最先发明的字符,在中文、日文、韩文和越南文的书写中使用。

    编码字符集是一个字符集,它为每一个字符分配一个唯一数字。Unicode标准的核心是一个编码字符集,字母“A”的编码为0041和字符“€”的编码为20AC。Unicode标准始终使用十六进制数字,而且在书写时在前面加上前缀“U+”,所以“A”的编码书写为“U+0041”。

    代码点是指可用于编码字符集的数字。编码字符集定义一个有效的代码点范围,但是并不一定将字符分配给所有这些代码点。有效的Unicode代码点范围是U+0000至U+10FFFF。Unicode 4.0将字符分配给一百多万个代码点中的96 382代码点。

    增补字符是代码点在U+10000至U+10FFFF范围之间的字符,也就是那些使用原始的Unicode的16位设计无法表示的字符。从U+0000至U+FFFF之间的字符集有时候被称为基本多语言面(BMP)。因此,每一个Unicode字符要么属于BMP,要么属于增补字符。

    字符编码方案是从一个或多个编码字符集到一个或多个固定宽度代码单元序列的映射。最常用的代码单元是字节,但是16位或32位整数也可用于内部处理。UTF-32、UTF-16和UTF-8是Unicode标准的编码字符集的字符编码方案。

    UTF-32即将每一个Unicode代码点表示为相同值的32位整数。很明显,它是内部处理最方便的表达方式,但是,如果作为一般字符串表达方式,则要消耗更多的内存。

    UTF-16使用一个或两个未分配的16位代码单元的序列对Unicode代码点进行编码。值U+0000至U+FFFF编码为一个相同值的16位单元。增补字符编码为两个代码单元,第一个单元来自于高代理范围(U+D800至U+DBFF),第二个单元来自于低代理范围(U+DC00至U+DFFF)。这在概念上可能看起来类似于多字节编码,但是其中有一个重要区别:值U+D800至U+DFFF保留用于UTF-16;没有这些值分配字符作为代码点。这意味着,对于一个字符串中的每个单独的代码单元,软件可以识别是否该代码单元表示某个单单元字符,或者是否该代码单元是某个双单元字符的第一个或第二单元。这相当于某些传统的多字节字符编码来说是一个显著的改进,在传统的多字节字符编码中,字节值0x41既可能表示字母“A”,也可能是一个双字节字符的第二个字节。

    UTF-8使用一至四个字节的序列对编码Unicode代码点进行编码。U+0000至U+007F使用一个字节编码,U+0080至U+07FF使用两个字节,U+0800至U+FFFF使用三个字节,而U+10000至U+10FFFF使用四个字节。UTF-8设计原理为:字节值0x00至0x7F始终表示代码点U+0000至U+007F(Basic Latin字符子集,它对应ASCII字符集)。这些字节值永远不会表示其他代码点,这一特性使UTF-8可以很方便地在软件中将特殊的含义赋予某些ASCII字符。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值