Java中的码点和代码单元
今天在翻阅 Java 书籍的时候,发现还有这么一块东西,以前一直没太注意。刚开始看的时候,感觉有点乱,后来看了这篇博客,理解了码点这个概念后,还是挺简单的。本篇后面部分的内容也是参考的这篇。
UTF-8 和 UTF-16 简单对比
首先明确一点,Unicode 只是字符集合,具体到如何将这些字符在计算机中存储,有 UTF-8、UTF-16 等众多编码方案。
UTF-8
变长字节(1 ~ 4 字节)。
编码规则:
- 单字节:字节的第一位设为 0,后面 7 位为这个符号的 Unicode 码,因此对于英文字母,UTF-8 编码和 ASCII 码是相同的。
- n(n > 1)字节:第一个字节的前 n 位都设为 1,第 n+1 位设为 0,后面字节的前两位一律设为 10,剩下的没有涉及的二进制位,以该字符的 Unicode 码填充。
UTF-16
变长字节。
编码规则:
- 编号 U+0000 到 U+FFFF 的字符(
常用字符集
),直接用两个字节表示。 - 编号 U+10000 到 U+10FFFF 之间的字符(
辅助字符集
),需要用四个字节表示。
相关概念
码点:一个有效的 Unicode 字符的二进制代码值被称作一个码点。码点一般用十六进制书写,并加上前缀 U+
。
代码单元:Unicode 常用字符都可以用两个字节表示,通常被称作代码单元(code unit)。辅助字符的编码需要两个连续的编码单元,即四字节。
简单来说,一个 Unicode 字符就是一个码点,Unicode 的常用字符对应一个代码单元,辅助字符对应两个代码单元。
Java 中的 char 使用的是 UTF-16 编码,占两个字节。因此,对于辅助字符集,char 的长度是不够的,所以 java 中不建议采用 char 类型保存字符,而建议使用 String。
以字符 𝕆
为例,它在 Unicode 字符集中的码点为 U+1D546。由 UTF-16 的编码规则知道,它算辅助字符集中的字符,因此需要两个代码单元来表示,使用 char 类型无法保存该字符。𝕆
在 Java 中的代码单元为 U+D835 和 U+DD46。
char ch = '𝕆'; // Too many characters in character literal
String s = "\uD835\uDD46"; // 𝕆
几个例子
-
length()
,返回给定字符串采用 UTF-16 编码表示时所需要的代码单元的数量String s1 = "Hello"; System.out.println(s1.length()); // 5 String s2 = "𝕆Hello"; System.out.println(s2.length()); // 7
-
codePointCount(beginIndex, lastIndex)
,返回指定范围内(不包括 lastIndex)的 Unicode 码点数量String s1 = "Hello"; System.out.println(s1.codePointCount(0, s1.length())); // 5 String s2 = "𝕆Hello"; System.out.println(s2.codePointCount(0, s2.length())); // 6
-
chartAt(i)
,返回位置 i(0 <= i < length() - 1) 的代码单元String s1 = "Hello"; System.out.println(s1.charAt(0)); // H String s2 = "𝕆Hello"; System.out.println(s2.charAt(0)); // ? 注意到这里,charAt 只获取到辅助字符的第一个代码单元,所以没有准确输出
-
offsetByCodePoints(index, codePointOffset)
,返回从指定 index 处偏移 codePointOffset 个码点的索引codePointAt(i)
,返回位置 i 处的码点String s = "𝕆Hello"; for (int i = 0; i < s.codePointCount(0, s.length()); i++) { int codePointIndex = s.offsetByCodePoints(0, i); int codePointValue = s.codePointAt(codePointIndex); System.out.printf("%d\t%d\n", codePointIndex, codePointValue); } // 输出: // 0 120134 // 2 72 // 3 101 // 4 108 // 5 108 // 6 111
这种用法乍一看比较繁琐,跟我们平时遍历字符串的的方法不太一样。实际上,若是字符串中包含辅助字符,则要以码点为单位而不是逐代码单元地递增遍历。
更容易的一种办法是使用
codePoints
,然后将字符串转换成一个 int 型数组:String s = "𝕆Hello"; int[] a = s.codePoints().toArray(); for (int i = 0; i < a.length; i++) { System.out.printf("%d\t%d\n", i, a[i]); }
结果同上面是一样的。
-
Character.isSupplementaryCodePoint(codePoint)
,判断指定的 Unicode 码点是否为辅助字符System.out.println(Character.isSupplementaryCodePoint(120134)); // true (120134 的十六进制为 1d546,对应辅助字符 𝕆 的码点) System.out.println(Character.isSupplementaryCodePoint(72)); // false (72 对应经典字符 H)
-
Character.isSurrogate(ch)
,判断 ch 对应的代码单元是否用于表示辅助字符String s = "𝕆Hello"; for (int i = 0; i < s.length(); i++) { // i = 0, 1, ..., 6 System.out.println(Character.isSurrogate(s.charAt(i))); // true true false false false false false }