今天在翻阅 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";	// 𝕆

几个例子

  1. length() ,返回给定字符串采用 UTF-16 编码表示时所需要的代码单元的数量

    String s1 = "Hello";
    System.out.println(s1.length());    // 5
    String s2 = "𝕆Hello";
    System.out.println(s2.length());    // 7
    
  2. 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
    
  3. 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 只获取到辅助字符的第一个代码单元,所以没有准确输出
    
  4. 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]);
    }
    

    结果同上面是一样的。

  5. Character.isSupplementaryCodePoint(codePoint)判断指定的 Unicode 码点是否为辅助字符

    System.out.println(Character.isSupplementaryCodePoint(120134)); // true (120134 的十六进制为 1d546,对应辅助字符 𝕆 的码点)
    System.out.println(Character.isSupplementaryCodePoint(72)); // false    (72 对应经典字符 H)
    
  6. 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
    }
    

参考