一、核心概念

1.1 Java 中的字符模型

  • char 类型:16 位无符号整数(UTF-16 编码单元),表示一个 Unicode 码元(Code Unit)。
  • Unicode:全球字符统一编码标准,每个字符有唯一码点(Code Point),范围 U+0000U+10FFFF
  • UTF-16:Java 内部使用的编码方式:
    • 基本多文种平面(BMP, U+0000 ~ U+FFFF):用 1 个 char 表示。
    • 辅助平面(U+10000 ~ U+10FFFF):用一对 char(代理对,Surrogate Pair)表示。

1.2 编码与解码

  • 编码(Encode):将字符(Unicode)转换为字节序列(如 UTF-8、GBK、ISO-8859-1)。
  • 解码(Decode):将字节序列转换为字符。

🔁 Java 中通过 StringCharset 实现编码转换,Character 类本身不直接提供编码转换方法,但它是字符操作的基础。


二、操作步骤(非常详细)

步骤 1:理解输入数据类型

确定你要转换的数据是:

  • char / Character
  • String
  • byte[](已编码的字节流)

步骤 2:从 String 转换为指定编码的 byte[](编码)

String text = "你好,世界!Hello World!";

// 方法 1:指定编码名称(推荐使用标准名称)
byte[] utf8Bytes = text.getBytes(StandardCharsets.UTF_8);
byte[] gbkBytes  = text.getBytes("GBK"); // 注意:可能抛 UnsupportedEncodingException

// 方法 2:使用 Charset 对象(更安全)
Charset gbkCharset = Charset.forName("GBK");
byte[] gbkBytes2 = text.getBytes(gbkCharset);

推荐使用 StandardCharsets.UTF_8 避免拼写错误和异常。

步骤 3:从 byte[] 转换为 String(解码)

// 假设 gbkBytes 是 GBK 编码的字节
String decodedStr = new String(gbkBytes, StandardCharsets.UTF_8); // ❌ 错误!应使用 GBK 解码
String correctStr = new String(gbkBytes, Charset.forName("GBK")); // ✅ 正确

⚠️ 必须使用正确的编码解码,否则出现乱码。

步骤 4:处理单个 char 的编码(间接方式)

Character 类不直接编码单个字符,但可通过 String 包装:

char ch = '中';

// 编码单个字符为 UTF-8 字节
byte[] bytes = String.valueOf(ch).getBytes(StandardCharsets.UTF_8);

// 解码字节为字符(需至少一个完整字符)
String str = new String(bytes, StandardCharsets.UTF_8);
char decodedChar = str.charAt(0);

步骤 5:处理代理对(Surrogate Pairs)

对于超出 BMP 的字符(如 emoji),需使用 int(Code Point)操作:

// 字符串包含 emoji
String emojiStr = "Hello 🌍!"; // 🌍 的码点是 U+1F30D

// 获取码点
int codePoint = emojiStr.codePointAt(1); // 'H'=0, 'e'=1, 但 emoji 占两个 char
int codePointAtEmoji = emojiStr.codePointAt(6); // 正确位置

// 将码点转为字符序列(可能生成两个 char)
String fromCodePoint = new String(Character.toChars(codePointAtEmoji));

// 遍历所有码点(推荐方式)
emojiStr.codePoints().forEach(cp -> {
    System.out.printf("Code Point: U+%04X%n", cp);
});

步骤 6:编码转换(如 GBK → UTF-8)

// 原始:GBK 编码的字节
byte[] gbkBytes = "中文".getBytes("GBK");

// 先解码为 Unicode(String)
String unicodeStr = new String(gbkBytes, Charset.forName("GBK"));

// 再编码为 UTF-8
byte[] utf8Bytes = unicodeStr.getBytes(StandardCharsets.UTF_8);

🔁 两步法bytes (编码A)String (Unicode)bytes (编码B)


三、常见错误

❌ 错误 1:未指定编码,使用平台默认

byte[] bytes = text.getBytes(); // 危险!依赖系统默认编码(Windows 可能是 GBK,Linux 可能是 UTF-8)
String str = new String(bytes); // 同样危险

解决:始终显式指定编码,如 StandardCharsets.UTF_8

❌ 错误 2:编码与解码编码不一致

byte[] bytes = "你好".getBytes("GBK");
String str = new String(bytes, StandardCharsets.UTF_8); // 乱码!

解决:确保编码和解码使用相同字符集。

❌ 错误 3:误用 Character 方法进行编码

// 错误:Character 没有 getBytes() 方法
// byte[] b = Character.getBytes('A'); // 编译错误!

正确:通过 String.valueOf(ch).getBytes(...)

❌ 错误 4:忽略代理对,使用 charAt() 遍历

String s = "A🌍B";
for (int i = 0; i < s.length(); i++) {
    char c = s.charAt(i);
    // 当 i 指向代理对的高位时,c 不是完整字符
}

解决:使用 codePoints()offsetByCodePoints()


四、注意事项

  1. 优先使用 StandardCharsets:如 StandardCharsets.UTF_8,类型安全、无异常。
  2. 避免硬编码编码名:使用常量或配置。
  3. 处理异常:使用 Charset.forName("XXX") 可能抛 UnsupportedCharsetException
  4. 内存与性能:编码转换会创建新对象,避免在高频循环中频繁转换。
  5. 国际化:始终使用 UTF-8 作为内部编码,避免乱码。
  6. 代理对:处理 emoji、罕见汉字时,必须使用 codePoint 相关方法。

五、使用技巧

✅ 技巧 1:封装编码转换工具方法

public class EncodingUtils {
    public static byte[] toUtf8Bytes(String str) {
        return str.getBytes(StandardCharsets.UTF_8);
    }

    public static String fromGbkBytes(byte[] bytes) {
        return new String(bytes, Charset.forName("GBK"));
    }

    public static byte[] convertEncoding(byte[] input, Charset from, Charset to) {
        String unicode = new String(input, from);
        return unicode.getBytes(to);
    }
}

✅ 技巧 2:使用 InputStreamReader / OutputStreamWriter 处理流

// 读取 GBK 文件
try (InputStreamReader reader = new InputStreamReader(
        new FileInputStream("file.txt"), Charset.forName("GBK"))) {
    BufferedReader br = new BufferedReader(reader);
    String line;
    while ((line = br.readLine()) != null) {
        // line 已是 Unicode String
    }
}

✅ 技巧 3:检测编码(第三方库)

Java 标准库不提供编码检测,可使用 juniversalchardetICU4J

// 使用 juniversalchardet 示例
byte[] bytes = Files.readAllBytes(Paths.get("file.txt"));
StringDetector detector = new StringDetector();
String encoding = detector.detect(bytes);

六、最佳实践

场景 推荐做法
内部存储与处理 使用 String(Unicode),编码无关
文件/网络传输 使用 UTF-8 编码
数据库连接 设置连接参数使用 UTF-8(如 ?useUnicode=true&characterEncoding=UTF-8
Web 应用 请求/响应设置 Content-Type: text/html; charset=UTF-8
配置文件 明确指定编码,避免平台差异
日志输出 使用 UTF-8,确保多语言字符正确显示

七、性能优化

✅ 1. 减少转换次数

// ❌ 低效:频繁转换
for (String s : list) {
    byte[] b = s.getBytes(StandardCharsets.UTF_8);
    outputStream.write(b);
}

// ✅ 高效:使用 Writer
try (OutputStreamWriter writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8)) {
    for (String s : list) {
        writer.write(s);
    }
}

✅ 2. 复用 Charset 对象

private static final Charset UTF8 = StandardCharsets.UTF_8; // 复用

✅ 3. 批量处理

使用 ByteBufferCharsetEncoder 批量编码,减少对象创建:

CharsetEncoder encoder = StandardCharsets.UTF_8.newEncoder();
CharBuffer charBuffer = CharBuffer.wrap("大量文本");
ByteBuffer byteBuffer = encoder.encode(charBuffer);

✅ 4. 避免在循环中创建 String

// ❌ 低效
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
    sb.append(new String(new byte[]{b}, StandardCharsets.UTF_8));
}

// ✅ 高效:整体解码
String str = new String(bytes, StandardCharsets.UTF_8);

总结

项目 关键要点
核心思想 Java 内部用 Unicode(UTF-16),I/O 用指定编码(如 UTF-8)
转换步骤 Stringbyte[] via Charset
关键方法 String.getBytes(Charset), new String(byte[], Charset)
最佳编码 内部用 Unicode,外部用 UTF-8
性能关键 减少转换、批量处理、复用编码器
避坑指南 显式指定编码、处理代理对、避免平台默认

💡 一句话掌握
Java 字符编码转换 = String(Unicode) 作为中枢,通过 getBytes()new String()字节流字符流 之间安全转换,始终指定编码,优先使用 UTF-8。