06、Java 语言的编码方案

Java 官方文档明确指出其字符字符串基于 Unicode 标准

  • Java 语言规范(JLS, Java Language Specification)文档第 3 章“词法结构” 中明确提到:
    • “The Java programming language represents text using sequences of 16-bit code units, using the UTF-16 encoding.”
    • Java 编程语言使用 16 位代码单元的序列来表示文本,采用 UTF-16 编码
      • Java 的 char 类型是 UTF-16 的 code unit,占用 2 字节(也就是 16 位)。
        • char 类型表示不了的字符,可以用字符串存储。
          • 如:😊 。
      • Java 的 字符串是 UTF-16 code unit 的序列

一、Java 中的字符 和 字符串采用 UTF-16 编码


1、char 字符

  • 每个 char 占 2 字节(16 位)
    • 可表示 Unicode 基本多语言平面的字符(即:BMP,U+0000~U+FFFF)。
  • 验证示例:
    • 验证 Java 的 char* 类型字符串的字符直接对应 Unicode 码点(UTF-16 编码)。
    • 输出结果与 Unicode 标准码点一致,说明 Java 内部使用 Unicode 存储字符
public static void javaChar() {
    // 基本多语言平面(BMP)字符
    char c1 = 'A';     // Unicode U+0041
    char c2 = 'あ';    // Unicode U+3042(日文平假名)
    char c3 = '€';     // Unicode U+20AC(欧元符号)

    // 输出字符的 Unicode 码点(十进制和十六进制)
    // A 的码点: 65 (Hex: 41)
    System.out.println("A 的码点: " + (int) c1 + " (十六进制: "
            + Integer.toHexString(c1) + ")");

    // あ 的码点: 12354 (Hex: 3042)
    System.out.println("あ 的码点: " + (int) c2 + " (十六进制: "
            + Integer.toHexString(c2) + ")");

    // € 的码点: 8364 (Hex: 20ac)
    System.out.println("€ 的码点: " + (int) c3 + " (十六进制: "
            + Integer.toHexString(c3) + ")");
}

2、Surrogate Pair(代理对)-- 两个 char 组成一个逻辑字符

  • 代理项(Surrogate):是一种仅在 UTF-16 中用来表示补充字符Unicode 扩展的字符)的方法。
    • 可表示 Unicode 扩展的字符(如:Emoji,U+10000 及以上)的一种方法
  • 在 UTF-16 中,为补充字符分配两个 16 位的 Unicode 代码单元:
    • 第一个代码单元:被称为 高代理项代码单元 前导代码单元
    • 第二个代码单元:被称为 低代理项代码单元尾随代码单元
  • 这两个代码单元组合在一起,就被称为代理项对****。

  • Java 中,一个 char 不能表示的字符,就使用 Surrogate Pair 来表示。
/**
 * 代理项对
 */
public static void surrogatePair(){
    // 使用代理对表示 Emoji 😊(U+1F60A)
    String emoji = "\uD83D\uDE0A";
    // 😊    --  说明:Java 允许直接在代码中使用 Unicode 转义符(\\uXXXX)表示字符
    System.out.println(emoji);

    // 输出字符长度、码点数量及实际码点
    // 字符串长度: 2    --  说明:使用两个 char(代理对)。
    System.out.println("字符串长度: " + emoji.length());

    // 码点数量: 1
    System.out.println("码点数量: " + emoji.codePointCount(0, emoji.length()));

    // 使用 String.codePointAt() 获取 Unicode 码点。
    int codePoint = emoji.codePointAt(0);

    // 实际码点: 1f60a  -- 码点 U+1F60A 符合 Unicode 标准,证明 Java 支持扩展字符。
    System.out.println("实际码点: " + Integer.toHexString(codePoint));

    char[] surrogates = Character.toChars(codePoint);
    // 输出 高位代理: D83D
    System.out.println("高位代理: " + Integer.toHexString((int) surrogates[0]).toUpperCase());
    // 输出 低位代理: DE0A
    System.out.println("低位代理: " + Integer.toHexString((int) surrogates[1]).toUpperCase());
}

3、字符串

  • Java 的 字符串是 UTF-16 code unit 的序列
public static void javaString(){
    // 十六进制码点:31
    System.out.println(Integer.toHexString((int)'1'));
    String str = "111";
    // 输出:[-2, -1, 0, 49, 0, 49, 0, 49]
    // BOM(字节顺序标记):UTF-16 编码默认添加 BOM。 即:FE FF(大端序),对应有符号字节为 -2 和 -1。
    System.out.println(Arrays.toString(str.getBytes(StandardCharsets.UTF_16)));
    // 大端输出:[0, 49, 0, 49, 0, 49]
    System.out.println(Arrays.toString(str.getBytes(StandardCharsets.UTF_16BE)));
    // 小端输出:[49, 0, 49, 0, 49, 0]
    System.out.println(Arrays.toString(str.getBytes(StandardCharsets.UTF_16LE)));

    // 字符串长度: 3.
    System.out.println("字符串长度: " + str.length());

    // 码点数量: 3
    System.out.println("码点数量: " + str.codePointCount(0, str.length()));

    // 实际码点: 31	31	31
    System.out.println("实际码点: " + Integer.toHexString(str.codePointAt(0))
            + "\t" + Integer.toHexString(str.codePointAt(0))
            + "\t" + Integer.toHexString(str.codePointAt(0)));
}
  • 注意:尽量不要用 UTF-16 编码
    • UTF-16 编码中,每个字符占用 2 字节,且可能用 BOM标识字节顺序大端序小端序)。
  • 字节顺序标记(Byte Order Mark,BOM):
    • 用来标识字节顺序大端序小端序)。
    • 即:FE FF(大端序),对应有符号字节为 -2 和 -1。
  • 示例:
// 输出:[-2, -1, 0, 49, 0, 49, 0, 49]
// BOM(字节顺序标记):UTF-16 编码默认添加 BOM。 
    // 即:FE FF(大端序),对应有符号字节为 -2 和 -1。
System.out.println(Arrays.toString(str.getBytes(StandardCharsets.UTF_16)));
// 大端输出:[0, 49, 0, 49, 0, 49]
System.out.println(Arrays.toString(str.getBytes(StandardCharsets.UTF_16BE)));
// 小端输出:[49, 0, 49, 0, 49, 0]
System.out.println(Arrays.toString(str.getBytes(StandardCharsets.UTF_16LE)));

  • 可以去除 BOM
/**
 * 将字符按照指定编码,转成 十六进制 字符串
 * @param param
 * @param charsetName
 * @return
 */
public static String getHexString(char param, String charsetName){
    byte[] bytes = null;
    try {
        bytes = String.valueOf(param).getBytes(charsetName);

        // 若必须使用 UTF-16,可手动去除 BOM(Byte Order Mark)。
        if (bytes.length >= 2 && bytes[0] == -2 && bytes[1] == -1) {
            bytes = Arrays.copyOfRange(bytes, 2, bytes.length);
        }

        StringBuffer buffer = new StringBuffer();
        for (byte b : bytes){
            buffer.append(String.format("%02X", b));
        }
        return buffer.toString();
    } catch (UnsupportedEncodingException e) {
        throw new RuntimeException(e);
    }
}

二、Java 中的其它编码

1、源文件 与 字节码 的编码

  • 源文件编码
    • 默认与操作系统 IDE 设置相关(如:UTF-8、GBK)。
    • 编译时会被统一转换为 UTF-8 格式的字节码(.class文件)。
  • 字节码文件:采用 UTF-8 编码存储,确保跨平台兼容性

2、与外部交互时的编码

  • 在与外部系统(文件、网络、数据库等)交互时,
    • 默认使用 平台编码(如:Windows 的 GBK,Linux/macOS 的 UTF-8)。
    • 但开发者可显式指定编码(如:UTF-8、ISO-8859-1)。

  • 字节流与字符流
    • InputStream/OutputStream 处理字节(无编码转换)。
    • Reader/Writer 处理字符(自动按指定编码转换字节字符)。
// 写入 UTF-8 文件
try (BufferedWriter writer = Files.newBufferedWriter(Paths.get("test.txt"), StandardCharsets.UTF_8)) {
    writer.write("你好,世界!");
}



// 读取 GBK 文件
try (BufferedReader reader = new BufferedReader(
    new InputStreamReader(new FileInputStream("data.txt"), "GBK"))) {
    String line = reader.readLine();
}

三、Java 编码方案的核心特点

1、全面支持 Unicode

  • 全球字符覆盖
    • Java 支持所有 Unicode 字符(当前 Unicode 14.0,共约 14 万字符)。
    • 涵盖几乎所有人类语言和符号(如:中文、阿拉伯文、数学符号、Emoji)。
  • 跨语言兼容性
    • 通过 UTF-16灵活的编码转换机制,确保多语言文本的正确处理。

2、平台独立性

  • 统一内部编码
    • UTF-16 作为内部标准,使 Java 程序在不同平台上运行时,字符处理逻辑一致。
  • 显式编码控制
    • 开发者可通过 Charset 类(如 StandardCharsets.UTF_8)指定外部数据的编码。
    • 避免平台差异导致的乱码问题。

3、灵活性与兼容性

  • 编码转换机制
    • 支持通过 CharsetEncoder 和 CharsetDecoder 实现任意编码间的转换。
      • 如:UTF-8 ↔ GBK 。
    • 等价于 使用 String.getBytes(charset) 和 new String(bytes, charset) 。
      • 无法精细控制错误处理
package org.rainlotus.materials.javabase.a01_unicode;

import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.*;
import java.util.Arrays;

/**
 * CharsetEncoder:将字符(CharBuffer)编码为字节(ByteBuffer)。
 * CharsetDecoder:将字节(ByteBuffer)解码为字符(CharBuffer)。
 * 适用场景:需要精确控制编码/解码过程(如错误处理策略、批量转换)。
 *
 * @author zhangxw
 */
public class CharsetEncoderAndDecoder {
    public static void main(String[] args) throws CharacterCodingException {
        // 定义字符集(以 UTF-8 为例)
        Charset charset = StandardCharsets.UTF_8;

        // 示例字符串(含无法用 ISO-8859-1 编码的中文字符)
        String originalText = "Hello 你好 😊";

        // ------------------------- 编码示例 -------------------------
        // 创建编码器,并设置错误处理策略:替换不可编码字符为 '?'
        CharsetEncoder encoder = charset.newEncoder()
                .onUnmappableCharacter(CodingErrorAction.REPLACE)
                .replaceWith("?".getBytes());

        // 将字符串转换为 CharBuffer
        CharBuffer charBuffer = CharBuffer.wrap(originalText);

        // 编码为 ByteBuffer
        ByteBuffer byteBuffer = encoder.encode(charBuffer);

        // 提取字节数组
        byte[] encodedBytes = new byte[byteBuffer.limit()];
        byteBuffer.get(encodedBytes);
        // 编码后的字节数组: [72, 101, 108, 108, 111, 32, -28, -67, -96, -27, -91, -67, 32, -16, -97, -104, -118]
        System.out.println("编码后的字节数组: " + Arrays.toString(encodedBytes));

        // ------------------------- 解码示例 -------------------------
        // 创建解码器,并设置错误处理策略:忽略无效字节
        CharsetDecoder decoder = charset.newDecoder()
                .onMalformedInput(CodingErrorAction.IGNORE)
                .onUnmappableCharacter(CodingErrorAction.REPLACE);

        // 将字节数组包装为 ByteBuffer
        ByteBuffer inputBuffer = ByteBuffer.wrap(encodedBytes);

        // 解码为 CharBuffer
        CharBuffer decodedBuffer = decoder.decode(inputBuffer);

        // 转换为字符串
        String decodedText = decodedBuffer.toString();
        // 解码后的字符串: Hello 你好 😊
        System.out.println("解码后的字符串: " + decodedText);


        System.out.println("\n\n");
        // 直接编码(无法自定义错误策略)
        byte[] bytes = originalText.getBytes(StandardCharsets.UTF_8);
        // 编码后的字节数组: [72, 101, 108, 108, 111, 32, -28, -67, -96, -27, -91, -67, 32, -16, -97, -104, -118]
        System.out.println("编码后的字节数组: " + Arrays.toString(bytes));

        // 直接解码(默认使用 REPLACE 策略)
        String text = new String(bytes, StandardCharsets.UTF_8);
        // 解码后的字符串: Hello 你好 😊
        System.out.println("解码后的字符串: " + text);
    }
}
  • 默认编码适配
    • System.getProperty(“file.encoding”) 获取平台默认编码。
    • 建议显式指定编码以增强可移植性。

4、内存与性能优化

  • 字符串压缩(Java 9+):引入 Compact Strings 优化。
  • Java 8 及之前
    • String内部用 char[](字符数组)存储数据,每个字符占用 2 字节(UTF-16 编码)。
      • 当字符串仅包含单字节字符(如:“Hello”)时,就会存在内存浪费
  • Java 9+
    • 改用 byte[](字节数组)存储数据,并引入 coder 标志位(1 字节)标识编码方式:
      • coder = 0:使用 Latin-1 编码(1 字节/字符),适用于单字节字符
        • 如:“Hello” 。
      • coder = 1:使用 UTF-16 编码(2 字节/字符),用于包含多字节字符的字符串。
        • 如:中文、Emoji 。
  • 自动检测编码,在创建字符串时
    • 若所有字符均在 Latin-1 范围内(0x00~0xFF),使用 coder=0(Latin-1 编码)。
      • ‌Latin-1 编码‌,也被称为‌ ISO-8859-1 编码
        • 是一种单字节字符编码标准,用于表示西欧语言中的字符。
        • 使用 8 位二进制数表示每个字符,总共可以表示 256 个不同的字符。
    • 若存在任何超出 Latin-1 的字符,则切换为 coder=1(UTF-16 编码)。
      • 如 ‘€’ 或中文。
  • 优化前后的对比示例:字符串 “Hello” 。
    • 在 Java 9+ 中仅占用 5 字节(Latin-1),而 Java 8 需要 10 字节(UTF-16)。
// Java 8 中:[0, 104, 0, 101, 0, 108, 0, 108, 0, 111]
System.out.println(Arrays.toString("hello".getBytes()));

// Java 9 中:[104, 101, 108, 108, 111]
System.out.println(Arrays.toString("hello".getBytes()));
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值