杰瑞科技汇

Java字符串如何正确转换为UTF-8编码?

Java 内部编码与外部编码

在深入代码之前,必须理解两个关键概念:

  1. Java 内部编码 (UTF-16)

    • Java 的 String 类在内存中是以 UTF-16 编码格式存储字符的。
    • 你可以认为每个 String 对象都是一个 UTF-16 的字符序列,这意味着当你处理字符串时(如 length(), charAt()),你操作的是 UTF-16 编码单元。
    • 对于大部分常见的英文字符、数字和拉丁符号,一个 UTF-16 编码单元(char)就足够了,但对于许多亚洲字符(如中文、日文、韩文)以及一些特殊符号,它们需要两个 UTF-16 编码单元(即一个“代理对”,surrogate pair)来表示。
  2. 外部编码 (如 UTF-8)

    • 当你需要将 String 对象写入文件、发送到网络、或存储在数据库中时,你不能直接传输 UTF-16 字节,因为这不仅效率低,而且不同系统(如 Windows/Linux)对字节序的处理可能不同。
    • 这时就需要将 String 转换为一种更通用、更节省空间的编码格式,UTF-8 是目前最主流的选择。
    • UTF-8 是一种变长编码,它可以用 1 到 4 个字节来表示一个 Unicode 字符。
      • ASCII 字符 (0-127) 占用 1 个字节。
      • 带变音符号的拉丁文、希腊文等占用 2 个字节。
      • 常见的中文、日文、韩文等占用 3 个字节。
      • 一些生僻的符号占用 4 个字节。

Java 程序内部的字符串是 UTF-16,但在与外部世界(文件、网络)交互时,通常需要将其编码(Encode)为 UTF-8 字节序列,反之,从外部读取数据时,需要将 UTF-8 字节序列解码(Decode)为 Java 内部的 UTF-16 String


将 String 编码为 UTF-8 字节数组

这是最常见的操作,例如将字符串写入文件或发送 HTTP 请求。

使用 String.getBytes(StandardCharsets.UTF_8)

这是最推荐、最安全的方式,显式指定字符集可以避免因系统默认编码不同而导致的乱码问题。

import java.nio.charset.StandardCharsets;
public class StringToUtf8 {
    public static void main(String[] args) {
        String originalString = "你好,世界!Hello, World! 😊";
        // 1. 将 String 编码为 UTF-8 字节数组
        // 这是推荐的做法,显式指定字符集
        byte[] utf8Bytes = originalString.getBytes(StandardCharsets.UTF_8);
        System.out.println("原始字符串: " + originalString);
        System.out.println("UTF-8 字节数组长度: " + utf8Bytes.length);
        System.out.println("UTF-8 字节数组内容: " + java.util.Arrays.toString(utf8Bytes));
        // 2. (不推荐) 使用系统默认编码
        // 这种方式可能会在不同操作系统上产生不同的结果,导致乱码。
        // byte[] defaultBytes = originalString.getBytes();
    }
}

输出分析:

  • 你好,世界!:每个中文字符在 UTF-8 中通常占 3 个字节,这里有 6 个汉字和 2 个标点,共 8 个字符,大约 24 个字节。
  • Hello, World!:这些是 ASCII 字符,每个占 1 个字节,共 13 个字符,13 个字节。
  • 这是一个 Emoji,在 UTF-8 中占 4 个字节。
  • 所以总字节数大约是 24 + 13 + 4 = 41 个字节。

将 UTF-8 字节数组解码为 String

这是从文件、网络等地方读取数据后的反向操作。

使用 new String(byte[], StandardCharsets.UTF_8)

同样,显式指定字符集是最佳实践。

import java.nio.charset.StandardCharsets;
public class Utf8ToString {
    public static void main(String[] args) {
        // 假设这是从网络或文件中读取到的 UTF-8 字节数组
        byte[] utf8Bytes = {(byte) 0xE4, (byte) 0xBD, (byte) 0xA0, (byte) 0xE5, (byte) 0xA5, (byte) 0xBD}; // "你好" 的 UTF-8 编码
        // 1. 将 UTF-8 字节数组解码为 String
        // 推荐做法,显式指定字符集
        String decodedString = new String(utf8Bytes, StandardCharsets.UTF_8);
        System.out.println("UTF-8 字节数组: " + java.util.Arrays.toString(utf8Bytes));
        System.out.println("解码后的字符串: " + decodedString);
        System.out.println("解码后字符串长度: " + decodedString.length()); // 输出 2
        // 2. (不推荐) 使用系统默认编码解码
        // 如果系统默认编码不是 UTF-8,这里可能会得到乱码
        // String wrongString = new String(utf8Bytes);
    }
}

使用 InputStreamOutputStream 进行文件读写

在实际应用中,我们通常不会手动处理字节数组,而是通过流(Stream)来高效地处理文件。

写入文件(编码)

使用 OutputStreamWriter,它可以自动将 String 编码为指定的字符集(如 UTF-8)再写入字节流。

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
public class WriteFileWithUtf8 {
    public static void main(String[] args) {
        String content = "这是使用 UTF-8 编码写入的文件。";
        String filePath = "output_utf8.txt";
        try (OutputStreamWriter writer = new OutputStreamWriter(
                new FileOutputStream(filePath), StandardCharsets.UTF_8)) {
            writer.write(content);
            System.out.println("文件写入成功: " + filePath);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

读取文件(解码)

使用 InputStreamReader,它可以自动从字节流中读取字节,并按照指定的字符集(如 UTF-8)解码为 String

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
public class ReadFileWithUtf8 {
    public static void main(String[] args) {
        String filePath = "output_utf8.txt"; // 使用上面代码创建的文件
        try (InputStreamReader reader = new InputStreamReader(
                new FileInputStream(filePath), StandardCharsets.UTF_8)) {
            StringBuilder sb = new StringBuilder();
            char[] buffer = new char[1024];
            int charsRead;
            // 循环读取,直到文件末尾
            while ((charsRead = reader.read(buffer)) != -1) {
                sb.append(buffer, 0, charsRead);
            }
            String content = sb.toString();
            System.out.println("从文件读取到的内容: " + content);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

字符集类 CharsetStandardCharsets

Java 7 引入了 java.nio.charset.StandardCharsets,它提供了一系列预定义的字符集常量,如 UTF_8, ISO_8859_1, US_ASCII 等。

为什么总是推荐使用 StandardCharsets.UTF_8

  1. 可读性和安全性:代码意图非常明确,避免了魔法字符串("UTF-8")带来的拼写错误风险。
  2. 性能StandardCharsets 中的常量是预加载的,直接使用它们比通过 Charset.forName("UTF-8") 查找要快。
  3. 健壮性Charset.forName() 在指定的字符集不存在时会抛出 IllegalCharsetNameExceptionUnsupportedCharsetException,使用 StandardCharsets 则完全避免了这个问题。
// 推荐
byte[] bytes = "test".getBytes(StandardCharsets.UTF_8);
String str = new String(bytes, StandardCharsets.UTF_8);
// 不推荐(容易出错且性能稍差)
// byte[] bytes = "test".getBytes("UTF-8"); // 如果拼写错误 "UFT-8" 会抛出异常
// String str = new String(bytes, "UTF-8");

常见问题与最佳实践

问题 1:为什么会出现乱码?

乱码的根本原因是 编码和解码时使用的字符集不一致

  • 场景:一台中文 Windows 服务器(默认编码可能是 GBK)将一个 String 写入文件时,没有指定编码,系统默认用 GBK 编码。
  • 问题:另一台 Linux 服务器(默认编码通常是 UTF-8)去读取这个文件时,如果也没有指定编码,它会尝试用 UTF-8 去解码。
  • 结果GBK 编码的字节序列被 UTF-8 解码器错误地解释,导致显示为乱码(如 或一堆不可读的符号)。

解决方案永远显式地指定字符集,在读写文件、网络通信、数据库连接等所有涉及编码转换的地方,都明确写出 StandardCharsets.UTF_8

问题 2:String.length() 返回的值是什么?

"你好".length() 返回 2,而不是 6(因为每个中文字符在 UTF-16 中可能占用 2 个 char)。 "😊".length() 返回 2,因为 Emoji 是一个代理对,由两个 char 组成。

如果你需要获取实际的 Unicode 字符数量(即用户感知的字符数),应该使用 codePointCount() 方法。

String emojiString = "😊";
System.out.println("length(): " + emojiString.length()); // 输出 2
System.out.println("codePointCount(): " + emojiString.codePointCount(0, emojiString.length())); // 输出 1
操作 推荐方法 说明
String -> UTF-8 Bytes str.getBytes(StandardCharsets.UTF_8) 核心编码方法,用于网络和文件输出。
UTF-8 Bytes -> String new String(bytes, StandardCharsets.UTF_8) 核心解码方法,用于处理网络和文件输入。
文件写入 new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8) 高效、正确的文件写入方式。
文件读取 new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8) 高效、正确的文件读取方式。
核心原则 永远显式指定字符集 避免依赖系统默认编码,这是防止乱码的黄金法则。
分享:
扫描分享到社交APP
上一篇
下一篇