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()));