杰瑞科技汇

Java中Unicode如何处理中文?

Unicode 和字符集

在深入 Java 之前,必须先理解几个关键概念:

Java中Unicode如何处理中文?-图1
(图片来源网络,侵删)
  1. 字符:一个抽象的符号,'A','中','€'。
  2. 编码:将字符映射成计算机可以处理的数字(二进制)的一套规则。
  3. 字符集:一个字符的集合,以及每个字符对应的唯一编号,这个编号就是 码点
    • Unicode 是目前最流行、最全面的字符集,它旨在为世界上所有的字符都分配一个唯一的码点,码点通常用 U+ 后跟十六进制数表示,'中' 的码点是 U+4E2D
  4. 编码方案:将 Unicode 码点转换为字节序列的具体实现方式。
    • UTF-8:变长编码,英文字符(ASCII 范围)占用 1 字节,中文、日文等常用字符通常占用 3 字节,生僻字符可能占用 4 字节。这是目前互联网上最主流的编码方式。
    • UTF-16:定长或变长编码,Java 语言内部使用 UTF-16 来表示字符串,基本多语言平面 的字符(包括绝大多数中文)固定占用 2 字节,辅助平面的字符(如某些生僻字或 emoji)占用 4 字节。
    • GBK / GB2312:中国国家标准编码,一个中文通常占用 2 字节。注意:这是中文环境下的历史遗留编码,在现代新项目中应避免使用,除非必须兼容非常老的系统。

Java 中的 Unicode 支持

Java 从设计之初就内置了对 Unicode 的强大支持,这是它的一大优势。

char 类型与 UTF-16

Java 的 char 数据类型是一个 16 位无符号整数,用来表示一个 UTF-16 代码单元。

  • 对于大多数中文字符(在 BMP 平面内),一个 char 就可以完整表示一个字符。
    char chineseChar = '中';
    System.out.println(chineseChar); // 输出: 中
    System.out.println((int)chineseChar); // 输出该字符的码点: 20013 (十进制) 或 0x4E2D (十六进制)
  • 对于需要 4 字节表示的字符(如某些 emoji 或生僻字),Java 使用 代理对 来表示,这由两个 char 组成。Character 类提供了处理这种情况的工具方法。

String

Java 的 String 类内部就是一个 char[] 数组,因此它天然就是基于 UTF-16 编码的。

String str = "Hello, 世界!";
System.out.println(str.length()); // 输出 9,因为 '世' 和 '界' 各占一个 char

转义字符

Java 源代码文件本身是使用某种编码保存的(通常是 UTF-8),但在代码中,你可以使用 Unicode 转义序列来直接表示任何字符。

Java中Unicode如何处理中文?-图2
(图片来源网络,侵删)

格式为:\u 后跟 4 位十六进制数。

// 这两种写法是等价的
char c1 = '中';
char c2 = '\u4E2D';
System.out.println(c1 == c2); // 输出 true

你也可以在 String 中使用:

String s1 = "你好";
String s2 = "\u4F60\u597D"; // \u4F60 是 '你', \u597D 是 '好'
System.out.println(s1.equals(s2)); // 输出 true

注意\u 转义是在 Java 编译器读取源代码时进行的,与源文件本身的编码无关。


常见问题与解决方案

问题 1:乱码

这是处理中文时最常见的问题,乱码的根本原因是 编码和解码时使用的编码不一致

Java中Unicode如何处理中文?-图3
(图片来源网络,侵删)

场景:从文件或网络读取中文

假设一个文本文件 test.txt 是用 GBK 编码保存的,内容是 "你好世界",但你用错误的编码(如 UTF-8)去读取它,就会得到乱码。

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.charset.Charset;
public class ReadChineseFile {
    public static void main(String[] args) {
        String filePath = "test.txt";
        // --- 错误示范:假设文件是 GBK,但用 UTF-8 读取 ---
        try {
            String contentWrong = new String(Files.readAllBytes(Paths.get(filePath)), StandardCharsets.UTF_8);
            System.out.println("用 UTF-8 读取 GBK 文件: " + contentWrong); // 输出乱码
        } catch (IOException e) {
            e.printStackTrace();
        }
        // --- 正确示范:使用正确的编码 GBK 读取 ---
        try {
            // 需要先获取系统支持的 GBK 编码,或使用第三方库如 Apache Commons Codec
            Charset gbkCharset = Charset.forName("GBK");
            String contentCorrect = new String(Files.readAllBytes(Paths.get(filePath)), gbkCharset);
            System.out.println("用 GBK 读取 GBK 文件: " + contentCorrect); // 输出: 你好世界
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

解决方案

  1. 明确数据源的编码:无论是文件、数据库还是网络请求,都要清楚数据最初是用什么编码保存或发送的。
  2. 在读取/写入时使用正确的 Charset
    • 推荐始终优先使用 StandardCharsets 枚举中预定义的常量,如 StandardCharsets.UTF_8
    • 对于特殊编码(如 GBK),使用 Charset.forName("GBK")
    • 避免:使用平台默认编码,如 String(byte[] bytes)Writer w = new FileWriter("file.txt");,因为不同平台的默认编码可能不同,导致程序可移植性变差。

问题 2:判断字符串中是否包含中文字符

一个简单的方法是判断字符的 Unicode 范围,中文字符的码点主要集中在 U+4E00U+9FFF 之间(CJK 统一表意文字)。

public class ContainsChinese {
    public static boolean isChinese(char c) {
        Character.UnicodeBlock ub = Character.UnicodeBlock.of(c);
        // 判断是否为中日韩文字
        return ub == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS
                || ub == Character.UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS
                || ub == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_A
                || ub == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_B
                || ub == Character.UnicodeBlock.CJK_SYMBOLS_AND_PUNCTUATION
                || ub == Character.UnicodeBlock.HALFWIDTH_AND_FULLWIDTH_FORMS
                || ub == Character.UnicodeBlock.GENERAL_PUNCTUATION;
    }
    public static boolean containsChinese(String str) {
        if (str == null) {
            return false;
        }
        for (int i = 0; i < str.length(); i++) {
            char c = str.charAt(i);
            if (isChinese(c)) {
                return true;
            }
        }
        return false;
    }
    public static void main(String[] args) {
        String s1 = "Hello, 世界!";
        String s2 = "Hello, World!";
        String s3 = "こんにちは"; // 日文平假名
        System.out.println(containsChinese(s1)); // true
        System.out.println(containsChinese(s2)); // false
        System.out.println(containsChinese(s3)); // false (根据上面的方法)
    }
}

注意str.length() 返回的是 char 的数量(UTF-16 代码单元数),对于包含代理对的字符(如 emoji),length() 会返回 2,如果需要获取字符的真正数量(码点数量),应该使用 str.codePointCount(0, str.length())

问题 3:遍历字符串中的字符(正确处理代理对)

如果字符串中可能包含 4 字节的字符,使用 for (int i = 0; i < str.length(); i++) 的方式遍历会有问题,因为它会把一个 4 字节的字符当成两个 char 来处理。

正确做法是使用 codePointAt 方法:

public class IterateString {
    public static void main(String[] args) {
        // 这是一个 emoji,由两个 char 组成的代理对
        String emoji = "😊";
        System.out.println("emoji.length() = " + emoji.length()); // 输出 2
        // 错误遍历方式
        System.out.println("--- 错误遍历 ---");
        for (int i = 0; i < emoji.length(); i++) {
            System.out.println(emoji.charAt(i));
        }
        // 输出两个无法识别的符号
        // 正确遍历方式
        System.out.println("--- 正确遍历 ---");
        for (int i = 0; i < emoji.length(); ) {
            int codePoint = emoji.codePointAt(i);
            System.out.println("码点: " + codePoint + ", 字符: " + new String(Character.toChars(codePoint)));
            i += Character.charCount(codePoint); // 根据是 1 个还是 2 个 char 来移动索引
        }
        // 输出:
        // 码点: 128522, 字符: 😊
    }
}

  1. 源代码编码:始终将你的 Java 源代码文件保存为 UTF-8 格式,现代 IDE(如 IntelliJ IDEA, Eclipse)都默认支持并推荐这样做。
  2. 内部处理:在 Java 程序内部,Stringchar 的 UTF-16 表示是透明的,你通常不需要关心,直接使用 String API 即可。
  3. I/O 操作
    • 读写文件:始终显式指定 Charset,优先使用 StandardCharsets.UTF_8
    • 网络通信:确保 HTTP 请求/响应头中包含正确的 Content-TypeCharset 信息,Content-Type: text/html; charset=utf-8
  4. 数据库交互:确保数据库、数据库连接、数据库表的字符集都设置为 utf8mb4(MySQL 中推荐,能完整支持 Unicode,包括 emoji)。
  5. 避免使用默认编码:不要依赖平台的默认编码,始终在 StringReaderWriterInputStreamOutputStream 的构造函数或方法中明确指定 Charset

遵循这些原则,你就可以在 Java 中游刃有余地处理包括中文在内的任何 Unicode 文本了。

分享:
扫描分享到社交APP
上一篇
下一篇