20210114 String 中的代码点和代码单元

String 中的代码点和代码单元

编码字符集 和 字符型编码

在给一个抽象字符集合中的每个字符都分配一个整数编号之后(注意这个整数并没有要求大小),这个字符集就有了顺序,就成为了 编码字符集

字符集编码 决定了如何将一个字符的整数编号对应到一个二进制的整数值,有的编码方案简单的将该整数值直接作为其在计算机中的表示而存储,例如英文字符就是这样,几乎所 有的字符集编码方案中,英文字母的整数编号与其在计算机内部存储的二进制形式都一致。但有的编码方案,例如适用于 Unicode 字符集的 UTF-8 编码形 式,就将很大一部分字符的整数编号作了变换后存储在计算机中。以“汉”字为例,“汉”的 Unicode 值为 0x6C49 ,但其编码为 UTF-8 格式后的值为 0xE6B189 (注意到变成了三个字节)

Unicode 编码

Unicode 是一种 编码字符集,而非 字符集编码

Unicode 的设计目标是包含世界上所有语言的字符

Unicode 最开始的设计是对所有字符采用 16 位编码,后来发现 16 位编码仅 65536 个码位,不能容纳世界上所有的字符,之后与 ISO 的 UCS 进行统一后,扩大了码空间,两者同意以一百一十万为限

Unicode 字符集,指的就是被分配了整数编号的字符集合,但要澄清的是,编码字符集中字符被分配的整数编号,不一定就是该字符在计算机中存储时所使用的值,计算机中存储的字符到底使用什么二进制整数值来表示,是由下面将要说到的字符集编码决定的

UTF-16 ,则对 Unicode 中的前 65536 个字符编号都不做变换,直接作为计算机存储时使用的值(对 65536 以后的字符,仍然要做变换),例如“汉”字的 Unicode 编号为 0x6C49 ,那么经过 UTF-16 编码后存储在计算机上时,它的表示仍为 0x6C49

UTF-16 提供了 surrogate pair 机制,使得 Unicode 中码位大于 65536 的那些字符得以表示,其基本的思想就是用两个 16 位的编码表示一个字符

Java 中的字符编码

【在 Java 中】,字符只以一种形式存在,就是 Unicode,注意这里没有选择特定的编码,直接使用它们在字符集中的编号,这是统一的唯一方法

【在 Java 中】 到底是指在哪里呢?就是指在 JVM 中,在内存中,在你的代码里声明的每一个 char , String 类型的变量中

JVM 的这种约定使得一个字符存在的世界分为了两部分: JVM 内部和 OS 的文件系统。在 JVM 内部,统一使用 Unicode 表示,当这个字符被从 JVM 内部移到外部(即保存为文件系统中的一个文件的内容时),就进行了编码转换,使用了具体的编码方案

JVM 的这种约定使得一个字符存在的世界分为了两部分: JVM 内部和 OS 的文件系统。在 JVM 内部,统一使用 Unicode 表示,当这个字符被从 JVM 内部移到外部(即保存为文件系统中的一个文件的内容时),就进行了编码转换,使用了具体的编码方案

IO、字节流、字符流

所有的编码转换就只发生在边界的地方,JVM和OS的交界处,也就是你的各种输入输出流(或者Reader,Writer类)起作用的地方

面向字节,那么这类工作要保证系统中的文件二进制内容和读入JVM内部的二进制内容要一致。不能变换任何0和1的顺序(也就是文件是怎么存的就怎么取,与文件保存的编码一致)。因此这是一种非常“忠实于原著”的做法

IO基本上可以分为两大阵营:面向字符的字符流,以及面向字节的字节流

面向字节,那么这类工作要保证系统中的文件二进制内容和读入JVM内部的二进制内容要一致。不能变换任何0和1的顺序(也就是文件是怎么存的就怎么取,与文件保存的编码一致)。

面向字符的IO是指希望系统中的文件的字符和读入内存的“字符”(注意和字节的区别)要一致。例 如我们的中文版 Windows XP 系统上有一个 GBK 的文本文件,其中有一个“汉”字,这个字的GBK编码是 0xBABA(而Unicode编号是 0x6C49),当我们使用面向字符的IO把它读入内存并保存在一个char型变量中时,我希望IO系统不要傻傻的直接把 0xBABA 放到这个char型 变量中,我甚至都不关心这个char型变量具体的二进制内容到底是多少,我只希望这个字符读进来之后仍然是“汉”这个字。

从这个意义上也可以看出,面向字符的IO类,也就是Reader和Writer类,实际上隐式的为我们做了编码转换,在输出时,将内存中的Unicode字符使用系统默认的编码方式进行了编码,而在输入时,将文件系统中已经编码过的字符使用默认编码方案进行了还原。

所谓编码转换就是一个字符与字节之间的转换,因此Java的IO系统中能够指定转换编码的地方,也就在字符与字节转换的地方,那就是 InputStreamReaderOutputStreamWriter!这两个类是字节流和字符流之间的适配器类,因此他们肩负着编码转换的任务简直太自然啦!

代码点( code point )和代码单元( code units )

代码点( code point ):是指编码字符集中,字符所对应的数字。有效范围从 U+0000U+10FFFF 。其中 U+0000U+FFFF 为基本字符(共 65536 个), U+10000U+10FFFF 为增补字符。

代码单元( code units ):对代码点进行编码得到的 1或2个16位序列 。其中 基本字符的代码点直接用一个相同值的代码单元表示,增补字符的代码点用两个代码单元的进行编码 ,这个范围内没有数字用于表示字符,因此程序可以识别出当前字符是单单元的基本字符,还是双单元的增补字符。

代码点(code point)是指与一个编码表中的某个字符对应的代码值。UTF-16 编码采用不同长度的编码表示所有Unicode 代码点,每个16位二进制表示一个代码单元(code unit)。基本字符的范围为 U+0000~U+FFFF ,辅助字符,即上面提到的增补字符,其两个代码单元的范围分别为 U+D800~U+DBFF (高代理部分,共1024个码位)和 U+DC00~U+DFFF (低代理部分,共1024个码位)。这样很容易就能知道一个代码单元是一个基本字符的编码还是一个辅助字符的第一或第二部分。

再想想刚才说过的 surrogate pair,一个 UTF-16 表示的增补字符(再一次的,需要两个char型变量才能表示的字符)怎样才能被正确的识别为增补字符,而不是两个普通的字符 呢?答案你也知道,就是通过看它的第一个char是不是在高代理范围内,第二个char是不是在低代理范围内来决定,这也意味着,高代理和低代理所占的共 2048个码位(从 0xD800 - 0xDFFF )是不能分配给其他字符的。

无论 Unicode 还是 UTF-16 编码后的 符,在 0x0000 - 0xFFFF 这个范围内,只有 63488 个字符。

代码点( Code Point )就是指 Unicode 中为字符分配的编号,一个字符只占一个代码点,例如我们说到字符 “汉”,它的代码点是 U+6C49。代码单元( Code Unit )则是针对编码方法而言,它指的是编码方法中对一个字符编码以后所占的最小存储单元。

一个字符,仅仅对应一个代码点,但却可能有多个代码单元

Java 中 String.length() 方法返回的是代码单元( code unit )的个数,而 String.codePointCount(0, length) 返回的是码点( code point )个数,即字符的个数。当然,通常这两个值是一致的。

代码示例

@Test
public void testString() {
    String s1 = "汉", s2 = "\ud87e\udc1a";
    System.out.println(s1 + "\t" + s2);
    // 长度比较
    int s1Len = s1.length();
    int s2Len = s2.length();
    System.out.println("String.length ==>\t" + s1Len + "\t" + s2Len);
    System.out.println("String.codePointCount ==>\t" + s1.codePointCount(0, s1Len) + "\t" + s2.codePointCount(0, s2Len));

    System.out.println("String.codePointAt ==>\t" + s1.codePointAt(0) + "\t" + s2.codePointAt(0));

    System.out.println("Character.toChars ==>\t" + Character.toChars(s1.codePointAt(0))[0] + "\t" + new String(Character.toChars(Character.toCodePoint(s2.charAt(0), s2.charAt(1)))));

}


===========================================
汉	𝕫
String.length ==>	1	2
String.codePointCount ==>	1	1
String.codePointAt ==>	27721	120171
Character.toChars ==>	汉	𝕫

结论

  • String 的内部是用 char[] 存储
  • Java 中一个 char 是 2 个字节(byte),16 位(bit)
  • length() 获取的是代码单元的个数,也就是字符串中 char 的个数
  • codePointCount() 获取的是代码点的个数,代码点是对标 Unicode 编码的,一个代码点代表 Unicode 中的一个字符,不同于 Java 中的 char
  • Java 中使用 1-2 个 char 组成一个 Unicode 的字符,如果 char 的范围在 0x0000 - 0xFFFF,则 1 个 char 对应一个 Unicode 字符,如果两个 char,前一个范围在 U+D800~U+DBFF,后一个范围在 U+DC00~U+DFFF,则两个 char 对应一个 Unicode 字符
  • 代码点和 char 是完全不同的概念,在二进制上不存在任何相等关系
  • 在上例中,s2 是由两个 char 组成的,分别是 \ud87e\udc1a,但是 s2 是一个代码点,编号数字是 120171

参考资料

posted @ 2021-01-14 17:26  流星<。)#)))≦  阅读(220)  评论(0编辑  收藏  举报