杰瑞科技汇

java 汉字 unicode

在 Java 中,char 类型使用 UTF-16 编码来表示字符,这意味着:

java 汉字 unicode-图1
(图片来源网络,侵删)
  1. 大部分常用汉字(基本多文种平面,BMP)可以用一个 char(16位)表示。
  2. 一些生僻汉字或特殊符号(辅助平面,Supplementary Planes)需要两个 char(一个“代理对”,Surrogate Pair)来表示。

Java 中的 char 和 Unicode

Java 语言设计之初,Unicode 标准还停留在 BMP(基本多文种平面,U+0000 到 U+FFFF)范围内,BMP 包含了世界上绝大多数语言的常用字符,其中就包括了几乎所有的常用汉字。

Java 的 char 类型被定义为 16 位无符号整数,正好可以存储 BMP 中的一个字符。

char c = '中'; // '中' 字的 Unicode 码点是 U+4E2D
System.out.println(c); // 输出: 中
System.out.println((int)c); // 输出: 20013 (这是 U+4E2D 的十进制表示)

在上面的例子中,'中' 这个字在 BMP 内部,所以它完美地适配一个 char


代理对:处理 BMP 之外的字符

随着 Unicode 标准的发展,字符数量大大增加,超出了 BMP 的范围,这些新增的字符被放置在“辅助平面”(从 U+10000 到 U+10FFFF),为了在仍然使用 16 位 char 的 Java 中表示这些字符,Unicode 标准引入了代理对(Surrogate Pair)机制。

java 汉字 unicode-图2
(图片来源网络,侵删)

代理对由两个特殊的 char 组成:

  • 高代理(High Surrogate / Leading Surrogate): 范围在 \uD800\uDBFF
  • 低代理(Low Surrogate / Trailing Surrogate): 范围在 \uDC00\uDFFF

一个辅助平面的字符码点 codePoint 可以通过以下公式转换为代理对:

  • highSurrogate = (char) ((codePoint - 0x10000) / 0x400 + 0xD800)
  • lowSurrogate = (char) ((codePoint - 0x10000) % 0x400 + 0xDC00)

例子:一个生僻字 "𠮷"

这个字(古同“吉”)的 Unicode 码点是 U+20BB7,它在辅助平面,无法用一个 char 表示。

// 这个字的 Unicode 码点
int codePoint = 0x20BB7;
// 在 Java 中,你必须用 char 数组或 String 来表示它
// 它实际上由两个 char 组成
String s = new String(new int[]{codePoint}, 0, 1);
// 获取这个字符串的长度
System.out.println(s.length()); // 输出: 2
// 获取构成它的两个 char
char[] chars = s.toCharArray();
System.out.println("高代理: " + Integer.toHexString(chars[0])); // 输出: d842
System.out.println("低代理: " + Integer.toHexString(chars[1])); // 输出: dfb7
// 正确地打印这个字
System.out.println(s); // 输出: 𠮷

关键点

  • s.length() 返回的是 2,因为它由两个 char 组成。
  • 如果错误地遍历 char 数组,你会得到两个无意义的“代理字符”,而不是正确的汉字。

正确处理汉字(推荐使用 int codePoint

由于代理对的存在,在处理字符串时,永远不要直接遍历 char 数组,你应该使用 codePointAt() 方法来获取正确的字符码点。

String text = "Hello 你好 𠮷";
// 错误的遍历方式:会错误地拆分生僻字
for (int i = 0; i < text.length(); i++) {
    char c = text.charAt(i);
    System.out.println("Index " + i + ": " + c + " (code point: " + (int)c + ")");
}
// 输出:
// Index 0: H (code point: 72)
// Index 1: e (code point: 101)
// ...
// Index 8: 你 (code point: 20320)
// Index 9: 好 (code point: 22909)
// Index 10:  (code point: 55362) <- 这是高代理,不是完整的字
// Index 11:  (code point: 57223) <- 这是低代理,不是完整的字
// 正确的遍历方式:使用 codePointAt
System.out.println("\n--- 正确遍历 ---");
for (int i = 0; i < text.length(); ) {
    int codePoint = text.codePointAt(i);
    // 判断这个码点是代表一个字符还是代理对
    int charCount = Character.charCount(codePoint);
    System.out.println("Code Point U+" + Integer.toHexString(codePoint) + 
                       " -> Character: " + (char)codePoint + 
                       ", Length in chars: " + charCount);
    // 移动索引,如果是代理对就移动2位,否则移动1位
    i += charCount;
}
// 输出:
// --- 正确遍历 ---
// Code Point U+48 -> Character: H, Length in chars: 1
// Code Point U+65 -> Character: e, Length in chars: 1
// ...
// Code Point U+4f60 -> Character: 你, Length in chars: 1
// Code Point U+597d -> Character: 好, Length in chars: 1
// Code Point U+20bb7 -> Character: 𠮷, Length in chars: 2

Stringchar[] 与字节数组的转换

在实际应用中,我们经常需要将 Java 的 String 与网络传输或文件存储的字节数组进行转换,这时就需要指定字符编码。

最重要的编码:UTF-8

UTF-8 是目前互联网上最通用的编码方式,它是一种变长编码

  • ASCII 字符(0-127)占用 1 个字节。
  • 汉字等常用字符(BMP 内)通常占用 3 个字节。
  • 辅助平面的字符(如 )占用 4 个字节。

String -> byte[] (编码)

String str = "你好,世界!𠮷";
// 使用 UTF-8 编码
byte[] utf8Bytes = str.getBytes(StandardCharsets.UTF_8);
System.out.println("UTF-8 字节数组长度: " + utf8Bytes.length); 
// "你好,世界!" 是 6个汉字 * 3字节 = 18字节
// "𠮷" 是 4字节
// 总长度是 22 字节
// 输出: UTF-8 字节数组长度: 22
// 如果不指定编码,会使用 JVM 默认字符集,这可能导致问题
byte[] defaultBytes = str.getBytes(); // 不推荐!

byte[] -> String (解码)

// 从字节数组解码回 String
String decodedStr = new String(utf8Bytes, StandardCharsets.UTF_8);
System.out.println("解码后的字符串: " + decodedStr);
// 输出: 解码后的字符串: 你好,世界!𠮷

其他编码(GBK/GB2312)

在中国大陆,有时还会遇到 GBKGB2312 编码,这些是双字节编码,主要用于表示简体中文。

  • 特点:一个汉字通常固定占用 2 个字节。
  • 问题:无法表示 这样的生僻字,也无法表示日文、韩文等其他语言的字符。

String -> byte[] (GBK 编码)

String gbkStr = "你好";
// 使用 GBK 编码
byte[] gbkBytes = gbkStr.getBytes("GBK");
System.out.println("GBK 字节数组长度: " + gbkBytes.length);
// 输出: GBK 字节数组长度: 4 (每个汉字 2 字节)

byte[] -> String (GBK 解码)

String decodedGbkStr = new String(gbkBytes, "GBK");
System.out.println("GBK 解码后的字符串: " + decodedGbkStr);
// 输出: GBK 解码后的字符串: 你好

总结与最佳实践

  1. 理解 char 的局限性:知道 char 只能表示 BMP 字符,生僻字需要代理对。
  2. 永远不要用 for(char c : str):遍历字符串时,使用 codePointAt()Character.charCount() 来确保正确处理所有字符。
  3. 显式指定编码:在进行 Stringbyte[] 转换时,永远显式地指定字符编码(如 StandardCharsets.UTF_8),不要依赖 JVM 的默认编码,这能避免绝大多数乱码问题。
  4. 优先使用 UTF-8:在 Web 开发、文件存储、网络通信等所有场景下,都优先使用 UTF-8 作为标准编码。
  5. 使用 String.codePointCount():如果你想获取一个字符串中有多少个字符(而不是多少个 char),使用这个方法。
    String s = "𠮷";
    System.out.println(s.length()); // 2
    System.out.println(s.codePointCount(0, s.length())); // 1
分享:
扫描分享到社交APP
上一篇
下一篇