“一个 char 就是一个字符”——这个直觉在 Java 中是错误的。一个 emoji 👨👩👧👦 在 Java 中占 11 个 char,但在用户眼中只是一个字符。本文将从 Unicode 的基础概念出发,深入剖析 Java 的字符编码机制,揭示 char、码点、代码单元、字形簇之间的层次关系。
Part 1: Unicode 基础——字符集的大一统
从 ASCII 到 Unicode 的演进
| 编码 |
年份 |
字符数 |
位宽 |
覆盖范围 |
| ASCII |
1963 |
128 |
7 位 |
英文字母、数字、控制字符 |
| ISO 8859-1 |
1987 |
256 |
8 位 |
西欧语言 |
| GB2312 |
1980 |
6,763 汉字 |
双字节 |
简体中文 |
| GBK |
1995 |
21,886 汉字 |
双字节 |
简繁中文 |
| Shift_JIS |
1982 |
~7,000 |
变长 |
日文 |
| Unicode 1.0 |
1991 |
7,161 |
16 位(最初设想) |
多语言 |
| Unicode 16.0 |
2024 |
154,998 |
21 位(实际) |
全球所有文字 + emoji |
Unicode 之前的世界是编码丛林:每种语言有自己的编码标准,同一个字节序列在不同编码下表示不同的字符,导致**乱码(Mojibake)**无处不在。
Unicode 的目标:为世界上每一个字符分配一个唯一的编号(码点)。
码点(Code Point)
码点是 Unicode 为每个字符分配的唯一编号,用 U+ 前缀加十六进制表示:
| 字符 |
码点 |
名称 |
| A |
U+0041 |
LATIN CAPITAL LETTER A |
| 中 |
U+4E2D |
CJK UNIFIED IDEOGRAPH-4E2D |
| 😀 |
U+1F600 |
GRINNING FACE |
| 𝄞 |
U+1D11E |
MUSICAL SYMBOL G CLEF |
码点范围:U+0000 到 U+10FFFF,共 1,114,112 个位置(21 位)。
Unicode 平面
Unicode 将码点空间划分为 17 个平面(Plane),每个平面 65,536(2^16)个码点:
代码平面(Code Plane)
Unicode 码点空间被分为 17 个平面(Plane 0-16),每个平面容纳 65,536 个码点。这 17 个平面共能表示 1,114,112 个字符。
| 平面 |
名称 |
码点范围 |
说明 |
包含字符示例 |
| Plane 0 |
BMP |
U+0000 - U+FFFF |
基本多文种平面 |
绝大多数常用字符(中文、英文、emoji 等) |
| Plane 1 |
SMP |
U+10000 - U+1FFFF |
补充多文种平面 |
历史文字、emoji、音乐符号等 |
| Plane 2 |
SIP |
U+20000 - U+2FFFF |
补充表意平面 |
扩展汉字 |
| Plane 3-13 |
- |
U+30000 - U+DFFFF |
保留 |
未使用 |
| Plane 14 |
SSP |
U+E0000 - U+EFFFF |
补充专用平面 |
用于私人自定义字符 |
| Plane 15-16 |
PUA |
U+F0000 - U+10FFFF |
私人使用区 |
用于私人自定义字符 |
BMP 之外的字符(U+10000 及以上)被称为增补字符(Supplementary Characters),它们在 Java 中需要两个 char 来表示。
Part 2: UTF-16 与 Java 的 char
为什么 Java 选择了 UTF-16
Java 诞生于 1995 年,当时 Unicode 还只有 65,536 个字符(BMP),设计者认为 16 位足以容纳所有字符。因此 Java 的 char 被设计为 16 位无符号整数,直接对应一个 Unicode 码点。
但 Unicode 2.0(1996 年)引入了增补平面,码点范围扩展到 U+10FFFF,超出了 16 位的表示能力。Java 不得不引入**代理对(Surrogate Pair)**机制来处理增补字符。
代理对(Surrogate Pair)
对于 BMP 之外的字符(U+10000 ~ U+10FFFF),UTF-16 使用两个 16 位代码单元来表示:
编码算法:
- 将码点减去 0x10000,得到一个 20 位的值
- 高 10 位加上 0xD800,得到高代理(High Surrogate):范围 0xD800 ~ 0xDBFF
- 低 10 位加上 0xDC00,得到低代理(Low Surrogate):范围 0xDC00 ~ 0xDFFF
示例:😀(U+1F600)的编码
1 2 3 4 5 6
| 码点:0x1F600 减去 0x10000:0xF600 = 0000 1111 0110 0000 0000(20 位) 高 10 位:0000 1111 01 = 0x3D → 高代理 = 0xD800 + 0x3D = 0xD83D 低 10 位:10 0000 0000 = 0x200 → 低代理 = 0xDC00 + 0x200 = 0xDE00
UTF-16 编码:0xD83D 0xDE00
|
代码单元 vs 码点 vs 字形簇
这是理解 Java 字符处理的关键层次模型:
1 2 3 4 5 6 7 8 9 10 11
| 层次 4: 字形簇(Grapheme Cluster)—— 用户看到的"一个字符" 👨👩👧👦 = 1 个字形簇
层次 3: 码点(Code Point)—— Unicode 分配的编号 👨 + ZWJ + 👩 + ZWJ + 👧 + ZWJ + 👦 = 7 个码点
层次 2: 代码单元(Code Unit)—— Java 的 char 每个 emoji 码点 = 2 个 char,ZWJ = 1 个 char → 共 11 个 char
层次 1: 字节(Byte)—— 存储单位 UTF-8 编码下每个 emoji = 4 字节 → 共 25 字节
|
| 概念 |
Java 类型 |
大小 |
示例 |
| 字节 |
byte |
8 位 |
UTF-8 的一个字节 |
| 代码单元 |
char |
16 位 |
UTF-16 的一个单元 |
| 码点 |
int |
21 位(存在 32 位 int 中) |
U+1F600 |
| 字形簇 |
无内置类型 |
可变 |
👨👩👧👦 |
Java 中的陷阱
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| String emoji = "😀";
System.out.println(emoji.length()); System.out.println(emoji.charAt(0)); System.out.println(emoji.charAt(1));
System.out.println(emoji.codePointCount(0, emoji.length())); System.out.println(emoji.codePointAt(0));
emoji.codePoints().forEach(cp -> { System.out.println("U+" + Integer.toHexString(cp).toUpperCase()); });
|
Part 3: UTF-8、UTF-16、UTF-32 对比
UTF-8 编码规则
UTF-8 是变长编码,使用 1-4 个字节表示一个码点:
| 码点范围 |
字节数 |
编码格式 |
示例 |
| U+0000 ~ U+007F |
1 |
0xxxxxxx |
A → 0x41 |
| U+0080 ~ U+07FF |
2 |
110xxxxx 10xxxxxx |
é → 0xC3 0xA9 |
| U+0800 ~ U+FFFF |
3 |
1110xxxx 10xxxxxx 10xxxxxx |
中 → 0xE4 0xB8 0xAD |
| U+10000 ~ U+10FFFF |
4 |
11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
😀 → 0xF0 0x9F 0x98 0x80 |
UTF-8 的优势:
- ASCII 兼容:ASCII 字符只需 1 字节,与 ASCII 编码完全相同
- 无字节序问题:不需要 BOM(Byte Order Mark)
- 自同步:UTF-8是一种变长编码方式,其特点在于能够通过前导字节和后续字节的不同模式来识别字符边界,从而实现对不同长度字符的有效解析与处理。
三种 UTF 编码对比
| 维度 |
UTF-8 |
UTF-16 |
UTF-32 |
| 代码单元大小 |
8 位 |
16 位 |
32 位 |
| 变长/定长 |
变长(1-4 字节) |
变长(2 或 4 字节) |
定长(4 字节) |
| ASCII 效率 |
最高(1 字节) |
低(2 字节) |
最低(4 字节) |
| 中文效率 |
中(3 字节) |
高(2 字节) |
低(4 字节) |
| emoji 效率 |
4 字节 |
4 字节 |
4 字节 |
| 字节序 |
无 |
有(BE/LE) |
有(BE/LE) |
| 使用场景 |
网络传输、文件存储 |
Java/C# 内部 |
内部处理(少用) |
BOM(Byte Order Mark)
UTF-16 和 UTF-32 有**大端(BE)和小端(LE)**两种字节序。BOM 是文件开头的特殊字节,用于标识字节序:
| 编码 |
BOM 字节 |
| UTF-8 |
EF BB BF(可选,不推荐) |
| UTF-16 BE |
FE FF |
| UTF-16 LE |
FF FE |
| UTF-32 BE |
00 00 FE FF |
| UTF-32 LE |
FF FE 00 00 |
Java 的 InputStreamReader 不会自动处理 BOM,需要手动跳过或使用第三方库。
Part 4: Java 字符串的内部实现
Java 9 之前:char[] 存储
1 2 3 4 5
| public final class String { private final char[] value; private int hash; }
|
每个字符固定占 2 字节,即使是纯 ASCII 字符串也是如此。
Java 9+:Compact Strings
Java 9 引入了**紧凑字符串(Compact Strings)**优化:
1 2 3 4 5 6
| public final class String { private final byte[] value; private final byte coder; private int hash; }
|
| coder 值 |
编码 |
每字符字节数 |
适用场景 |
| 0 (LATIN1) |
ISO 8859-1 |
1 字节 |
纯 ASCII/Latin-1 字符串 |
| 1 (UTF16) |
UTF-16 |
2 字节 |
包含非 Latin-1 字符 |
内存节省:对于英文为主的应用,字符串内存占用减少约 40-50%。
字符串的不可变性
Java 的 String 是不可变的(immutable):
1 2
| String s = "Hello"; s = s + " World";
|
不可变性的优势:
- 线程安全:多线程可以安全地共享字符串
- 哈希缓存:hashCode 只需计算一次
- 字符串池:相同内容的字符串可以共享同一个对象
- 安全性:作为 HashMap 的 key、类名、文件路径等不会被意外修改
字符串池(String Pool)
1 2 3 4 5 6 7 8
| String s1 = "Hello"; String s2 = "Hello"; String s3 = new String("Hello"); String s4 = s3.intern();
System.out.println(s1 == s2); System.out.println(s1 == s3); System.out.println(s1 == s4);
|
Java 7 之前,字符串池位于永久代(PermGen);Java 7+ 移至堆(Heap),避免了 PermGen OOM。
String.intern() 的性能陷阱与调优
String.intern() 方法会将字符串放入字符串池并返回池中的引用。在高性能场景下使用 intern() 需要注意以下问题:
性能陷阱:
- 锁竞争:字符串池内部使用同步机制,高并发场景下可能成为瓶颈
- 内存压力:大量使用
intern() 会导致字符串池膨胀,增加 GC 压力
- Full GC 频繁:字符串池在老年代,频繁扩容可能触发 Full GC
调优参数:
1 2 3 4 5
| -XX:StringTableSize=100000
-XX:+PrintStringTableStatistics
|
最佳实践:
- 仅对重复出现的高频字符串使用
intern()(如配置键、标识符)
- 避免在循环中对动态生成的字符串调用
intern()
- 考虑使用
ConcurrentHashMap 等替代方案实现自定义缓存
Part 5: 正确处理 Unicode 的 Java API
码点级别的操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| String text = "Hello 😀 世界";
for (int i = 0; i < text.length(); i++) { char c = text.charAt(i); }
text.codePoints().forEach(codePoint -> { System.out.println( "U+" + Integer.toHexString(codePoint).toUpperCase() + " → " + new String(Character.toChars(codePoint)) ); });
int charCount = text.length(); int codePointCount = text.codePointCount(0, text.length());
|
码点与 char 的转换
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| int codePoint = 0x1F600; char[] chars = Character.toChars(codePoint);
boolean isSupplementary = Character.isSupplementaryCodePoint(codePoint);
boolean isHighSurrogate = Character.isHighSurrogate('\uD83D'); boolean isLowSurrogate = Character.isLowSurrogate('\uDE00');
int restored = Character.toCodePoint('\uD83D', '\uDE00');
|
字符串反转的正确实现
1 2 3 4 5 6 7 8 9 10 11 12 13
| String broken = new StringBuilder("Hello 😀").reverse().toString();
public static String reverseByCodePoints(String input) { int[] codePoints = input.codePoints().toArray(); StringBuilder result = new StringBuilder(); for (int i = codePoints.length - 1; i >= 0; i--) { result.appendCodePoint(codePoints[i]); } return result.toString(); }
|
注意:StringBuilder.reverse() 从 Java 1.5 开始就正确处理了代理对。
字符串截取的正确实现
1 2 3 4 5 6 7 8 9 10 11 12
| String text = "A😀B"; String sub = text.substring(0, 2);
public static String substringByCodePoints(String str, int beginIndex, int endIndex) { int charBegin = str.offsetByCodePoints(0, beginIndex); int charEnd = str.offsetByCodePoints(0, endIndex); return str.substring(charBegin, charEnd); }
substringByCodePoints("A😀B", 0, 2);
|
Part 6: 字形簇——用户眼中的"字符"
什么是字形簇
字形簇(Grapheme Cluster) 是用户在屏幕上看到的一个"视觉字符",它可能由多个码点组成:
| 字形簇 |
码点组成 |
码点数 |
char 数 |
| é |
U+0065 + U+0301(e + 组合重音符) |
2 |
2 |
| 👨👩👧👦 |
U+1F468 + U+200D + U+1F469 + U+200D + U+1F467 + U+200D + U+1F466 |
7 |
11 |
| 🇨🇳 |
U+1F1E8 + U+1F1F3(区域指示符 C + N) |
2 |
4 |
| 한 |
U+1112 + U+1161 + U+11AB(ᄒ + ᅡ + ᆫ 组合) |
3 |
3 |
ZWJ(零宽连接符)
ZWJ(Zero Width Joiner,U+200D) 是一个不可见字符,用于将多个 emoji 连接成一个复合 emoji:
1 2 3 4
| 👨 + ZWJ + 💻 = 👨💻(男性技术工作者) 👩 + ZWJ + 🔬 = 👩🔬(女性科学家) 👨 + ZWJ + 👩 + ZWJ + 👧 + ZWJ + 👦 = 👨👩👧👦(家庭) 🏳 + ZWJ + 🌈 = 🏳️🌈(彩虹旗)
|
组合字符与预组合字符
同一个视觉字符可能有多种 Unicode 表示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| String nfc = "\u00E9";
String nfd = "e\u0301";
System.out.println(nfc.equals(nfd)); System.out.println(nfc.length()); System.out.println(nfd.length());
String normalizedNfc = Normalizer.normalize(nfc, Normalizer.Form.NFC); String normalizedNfd = Normalizer.normalize(nfd, Normalizer.Form.NFC); System.out.println(normalizedNfc.equals(normalizedNfd));
|
Unicode 正规化形式
| 形式 |
全称 |
说明 |
| NFC |
Canonical Decomposition + Canonical Composition |
先分解再组合(推荐) |
| NFD |
Canonical Decomposition |
完全分解 |
| NFKC |
Compatibility Decomposition + Canonical Composition |
兼容分解再组合 |
| NFKD |
Compatibility Decomposition |
兼容分解 |
NFC 是最常用的形式,Web 标准(HTML5)推荐使用 NFC。
Java 中处理字形簇
Java 标准库没有直接提供字形簇 API,但可以使用 BreakIterator:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| import java.text.BreakIterator;
String family = "👨👩👧👦";
System.out.println(family.length());
System.out.println(family.codePointCount(0, family.length()));
BreakIterator iterator = BreakIterator.getCharacterInstance(); iterator.setText(family); int graphemeCount = 0; while (iterator.next() != BreakIterator.DONE) { graphemeCount++; } System.out.println(graphemeCount);
|
Part 7: 编码转换与乱码
Java 中的编码转换
1 2 3 4 5 6 7 8 9 10 11 12
| String text = "你好世界";
byte[] utf8Bytes = text.getBytes(StandardCharsets.UTF_8); byte[] gbkBytes = text.getBytes(Charset.forName("GBK"));
System.out.println(utf8Bytes.length); System.out.println(gbkBytes.length);
String fromUtf8 = new String(utf8Bytes, StandardCharsets.UTF_8); String garbled = new String(utf8Bytes, Charset.forName("GBK"));
|
常见乱码场景与解决方案
| 场景 |
原因 |
解决方案 |
| 网页乱码 |
Content-Type 未指定或指定错误 |
设置 Content-Type: text/html; charset=UTF-8 |
| 数据库乱码 |
数据库/连接/表的字符集不一致 |
统一使用 utf8mb4(MySQL) |
| 文件乱码 |
文件编码与读取编码不匹配 |
使用 BOM 或显式指定编码 |
| 控制台乱码 |
控制台编码与程序输出编码不匹配 |
设置 -Dfile.encoding=UTF-8 |
CharsetEncoder 与 CharsetDecoder 的错误处理策略
Java 提供了 CharsetEncoder 和 CharsetDecoder 用于精细控制编码转换过程,支持多种错误处理策略:
CharsetEncoder 处理策略:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| CharsetEncoder encoder = StandardCharsets.UTF_8.newEncoder();
encoder.onMalformedInput(CodingErrorAction.REPLACE); encoder.onUnmappableCharacter(CodingErrorAction.REPLACE);
encoder.onMalformedInput(CodingErrorAction.REPORT); encoder.onUnmappableCharacter(CodingErrorAction.REPORT);
encoder.onMalformedInput(CodingErrorAction.IGNORE); encoder.onUnmappableCharacter(CodingErrorAction.IGNORE);
try { ByteBuffer buffer = encoder.encode(CharBuffer.wrap("测试字符串")); } catch (CharacterCodingException e) { }
|
CharsetDecoder 处理策略:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder();
decoder.onMalformedInput(CodingErrorAction.REPLACE); decoder.onUnmappableCharacter(CodingErrorAction.REPLACE);
decoder.onMalformedInput(CodingErrorAction.REPORT); decoder.onUnmappableCharacter(CodingErrorAction.REPORT);
decoder.onMalformedInput(CodingErrorAction.IGNORE); decoder.onUnmappableCharacter(CodingErrorAction.IGNORE);
try { CharBuffer buffer = decoder.decode(ByteBuffer.wrap(bytes)); } catch (CharacterCodingException e) { }
|
错误类型说明:
- malformed-input:输入字节序列不符合编码规范(如 UTF-8 无效字节)
- unmappable-character:字符无法映射到目标编码(如 GBK 无法表示的 Unicode 字符)
最佳实践:
- 生产环境建议使用
REPLACE 策略,避免因个别字符导致整个流程失败
- 对数据完整性要求高的场景使用
REPORT 策略并显式处理异常
- 自定义替换字符:
encoder.replaceWith(new byte[]{(byte)'?'})
MySQL 的 utf8 vs utf8mb4
MySQL 的 utf8 编码实际上仅支持 3 字节(即 BMP 字符)。要存储 emoji 等增补字符,必须使用 utf8mb4:
1 2 3 4 5 6 7 8 9 10 11
| CREATE TABLE messages ( content VARCHAR(255) CHARACTER SET utf8 ); INSERT INTO messages VALUES ('Hello 😀');
CREATE TABLE messages ( content VARCHAR(255) CHARACTER SET utf8mb4 ); INSERT INTO messages VALUES ('Hello 😀');
|
文件读写的编码处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| try (BufferedReader reader = new BufferedReader( new InputStreamReader( new FileInputStream("data.txt"), StandardCharsets.UTF_8 ))) { String line; while ((line = reader.readLine()) != null) { System.out.println(line); } }
String content = Files.readString(Path.of("data.txt"), StandardCharsets.UTF_8);
String content = Files.readString(Path.of("data.txt"));
|
Part 8: 正则表达式与 Unicode
Java 正则的 Unicode 支持
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| Pattern unicodeLetter = Pattern.compile("\\p{L}+"); Matcher matcher = unicodeLetter.matcher("Hello 你好 مرحبا"); while (matcher.find()) { System.out.println(matcher.group()); }
Pattern emojiPattern = Pattern.compile("[\\x{1F600}-\\x{1F64F}]");
|
UNICODE_CHARACTER_CLASS 标志
1 2 3 4 5 6 7
| Pattern asciiWord = Pattern.compile("\\w+");
Pattern unicodeWord = Pattern.compile("\\w+", Pattern.UNICODE_CHARACTER_CLASS);
|
大小写不敏感匹配
1 2 3 4 5 6 7
| Pattern.compile("hello", Pattern.CASE_INSENSITIVE);
Pattern.compile("hello", Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE);
|
Part 9: 国际化(i18n)实践
Locale 与字符串比较
1 2 3 4 5 6 7 8 9 10
| String text = "TITLE"; Locale turkish = new Locale("tr", "TR");
System.out.println(text.toLowerCase()); System.out.println(text.toLowerCase(turkish));
String german = "straße"; System.out.println(german.toUpperCase(Locale.GERMAN));
|
Collator:语言感知的排序
1 2 3 4 5 6 7 8 9
| List<String> words = Arrays.asList("café", "caff", "cache"); Collections.sort(words);
Collator frenchCollator = Collator.getInstance(Locale.FRENCH); words.sort(frenchCollator);
|
字符串长度限制的正确实现
在数据库或 API 中限制字符串长度时,需要明确"长度"的定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public static int countGraphemeClusters(String text) { BreakIterator iterator = BreakIterator.getCharacterInstance(); iterator.setText(text); int count = 0; while (iterator.next() != BreakIterator.DONE) { count++; } return count; }
String text = "Hello 👨👩👧👦"; System.out.println(text.length()); System.out.println(text.codePointCount(0, text.length())); System.out.println(countGraphemeClusters(text)); System.out.println(text.getBytes(StandardCharsets.UTF_8).length);
|
总结
| 层次 |
概念 |
Java 表示 |
大小 |
用途 |
| 字节 |
存储单元 |
byte |
8 位 |
文件/网络 I/O |
| 代码单元 |
编码单元 |
char |
16 位 |
Java 内部存储 |
| 码点 |
Unicode 编号 |
int |
21 位 |
字符标识 |
| 字形簇 |
视觉字符 |
BreakIterator |
可变 |
用户界面 |
核心原则:
- 不要假设一个
char 就是一个字符
- 遍历字符串时使用
codePoints() 而非 charAt()
- 比较字符串前先进行 Unicode 正规化
- 存储和传输统一使用 UTF-8
- 数据库使用
utf8mb4 而非 utf8
Java 17+ 的 Unicode 版本支持情况
Java 平台随着版本更新持续升级 Unicode 标准支持:
| Java 版本 |
Unicode 版本 |
主要更新 |
| Java 8 |
Unicode 6.2.0 |
基础 Unicode 支持 |
| Java 9 |
Unicode 8.0.0 |
新增 7,500+ 字符,包括 emoji 5.0 |
| Java 11 |
Unicode 10.0.0 |
新增 8,518 字符,包括 emoji 11.0 |
| Java 12 |
Unicode 11.0.0 |
新增 684 字符,包括 emoji 11.0 |
| Java 13 |
Unicode 12.1.0 |
新增 554 字符,包括 emoji 12.0 |
| Java 15 |
Unicode 13.0.0 |
新增 5,930 字符,包括 emoji 13.0 |
| Java 17 |
Unicode 13.0.0 |
长期支持版本 |
| Java 21 |
Unicode 15.0.0 |
新增 4,489 字符,包括 emoji 15.0 |
重要影响:
- 新字符支持:新版本支持更多语言字符、符号和 emoji
- 规范化变化:新版本可能引入新的规范化规则
- 性能优化:新版本通常包含字符处理性能改进
版本兼容性建议:
- 需要最新 emoji 支持的应用建议使用 Java 21+
- 企业级应用可使用 Java 17 LTS 版本(Unicode 13.0)
- 如需处理特定 Unicode 版本内容,应验证目标 Java 版本的支持情况
检查当前 Unicode 版本:
1 2
| Character.UnicodeBlock block = Character.UnicodeBlock.of('你'); System.out.println("Unicode 版本信息可通过 Character 类获取");
|
参考资料