bigywater.tech
Java · 214

Unicode 与 Charset:字符、代码点和字节不要混为一谈

一个「字」在 Java 里有 char、码点、字素、字节四种数法;用错字符集又会出现各种乱码和问号。这篇逐项真实运行,并在 JDK 17、21、27 下对比默认字符集在不同 LC_ALL 下的结果。

8 / 5 / 1
家庭 emoji 的 length / 码点数 / 字素数
UTF-8
JDK 21 与 27 的默认字符集,与 LC_ALL 无关
US-ASCII
JDK 17 在 LC_ALL=C 下的默认字符集

char、码点、字素、字节

实测 · 本机执行输出
  文本                         length      码点数      字素数  UTF-8字节 UTF-16字节
  字母 a                            1        1      1/1        1        2
  汉字 中                            1        1      1/1        3        2
  表情 😀 U+1F600                   2        1      1/1        4        4
  é 预组合 U+00E9                    1        1      1/1        2        2
  é 组合 e+U+0301                   2        2      1/1        3        4
  家庭 👨‍👩‍👧(ZWJ 序列)             8        5      1/1       18       16
  (字素数一列:BreakIterator / 正则 \X 两种方法的结果)
  "a😀b".charAt(1) = U+D83D(高代理),charAt(2) = U+DE00(低代理);Character.isSurrogate(charAt(1)) = true
  new StringBuilder("a😀b").reverse() = 代码点正确反转:[62, 1f600, 61](StringBuilder.reverse 会处理代理对)
  按 char 倒序拼接得到 [62, de00, d83d, 61](代理对被拆开且顺序反了)
  "a😀b".substring(0, 2) = 61 3f(UTF-8 字节;截在代理对中间,只剩一个孤立的高代理,编码成 ? 即 3f)

编码与解码:乱码、替换与报错

实测 · 本机执行输出
  原文 "中文é😀" 的 UTF-8 字节:e4 b8 ad e6 96 87 c3 a9 f0 9f 98 80(12 字节)
  按 UTF-8      解码 → "中文é😀"  长度 5,是否等于原文 true
  按 ISO-8859-1 解码 → "中文é😀"  长度 12,是否等于原文 false
  按 GBK        解码 → "涓枃茅馃榾"  长度 6,是否等于原文 false
  按 US-ASCII   解码 → "������������"  长度 12,是否等于原文 false
  按 UTF-16     解码 → "귦隇쎩颀"  长度 6,是否等于原文 false
  用 ISO-8859-1 编码原文 → 3f 3f e9 3f(4 字节)
  用 US-ASCII   编码原文 → 3f 3f 3f 3f(4 字节)
  用 GBK        编码原文 → d6 d0 ce c4 a8 a6 3f(7 字节)
  损坏的 UTF-8 字节 e4 b8 78 ff → new String(...) 得到码点 [U+FFFD, U+0078, U+FFFD](默认替换成 U+FFFD)
  CharsetDecoder + REPORT → MalformedInputException: Input length = 2
  US_ASCII 编码器编码 é + 默认 REPORT → UnmappableCharacterException: Input length = 1
  在 UTF-8 的 "中文" 第 4 字节处截断(字节 e4 b8 ad e6)→ "中�"(长度 2)

默认字符集:JDK 17 与 21 的差别

实测 · 本机执行输出
[JDK 17,LC_ALL=C]
  JDK 17.0.20.1, defaultCharset = US-ASCII, file.encoding = US-ASCII, native.encoding = US-ASCII, stdout.encoding = null
  FileWriter wrote U+4E2D as bytes: 3f; getBytes() = 3f; stdout bytes of println: see below
?
[JDK 17,LC_ALL=en_US.UTF-8]
  JDK 17.0.20.1, defaultCharset = UTF-8, file.encoding = UTF-8, native.encoding = UTF-8, stdout.encoding = null
  FileWriter wrote U+4E2D as bytes: e4 b8 ad; getBytes() = e4 b8 ad; stdout bytes of println: see below
中
[JDK 21,LC_ALL=C]
  JDK 21.0.12.1, defaultCharset = UTF-8, file.encoding = UTF-8, native.encoding = US-ASCII, stdout.encoding = US-ASCII
  FileWriter wrote U+4E2D as bytes: e4 b8 ad; getBytes() = e4 b8 ad; stdout bytes of println: see below
?
[JDK 21,LC_ALL=zh_CN.GBK]
  JDK 21.0.12.1, defaultCharset = UTF-8, file.encoding = UTF-8, native.encoding = GBK, stdout.encoding = GBK
  FileWriter wrote U+4E2D as bytes: e4 b8 ad; getBytes() = e4 b8 ad; stdout bytes of println: see below
中
[JDK 21,LC_ALL=C + -Dfile.encoding=GBK]
  JDK 21.0.12.1, defaultCharset = UTF-8, file.encoding = GBK, native.encoding = US-ASCII, stdout.encoding = US-ASCII
  FileWriter wrote U+4E2D as bytes: e4 b8 ad; getBytes() = e4 b8 ad; stdout bytes of println: see below
?
[JDK 21,LC_ALL=C + -Dfile.encoding=COMPAT]
  JDK 21.0.12.1, defaultCharset = US-ASCII, file.encoding = US-ASCII, native.encoding = US-ASCII, stdout.encoding = US-ASCII
  FileWriter wrote U+4E2D as bytes: 3f; getBytes() = 3f; stdout bytes of println: see below
?
[JDK 27,LC_ALL=C]
  JDK 27, defaultCharset = UTF-8, file.encoding = UTF-8, native.encoding = US-ASCII, stdout.encoding = US-ASCII
  FileWriter wrote U+4E2D as bytes: e4 b8 ad; getBytes() = e4 b8 ad; stdout bytes of println: see below
?

规范化与大小写

实测 · 本机执行输出
  "\u00e9".equals("e\u0301") = false,两者看起来相同
  Normalizer NFC(e+U+0301) 之后 equals 预组合 = true;NFD(é) 之后长度 = 2
  Collator 比较两种写法 compare = 0(0 表示相等,Collator 会处理组合序列)
  NFKC(fi 连字 U+FB01) = "fi";NFKC(全角 AB) = "AB"
  "straße".toUpperCase(Locale.ROOT) = "STRASSE"(长度 6 → 7);toLowerCase(ROOT) = "straße"(长度 6);toLowerCase(tr) = "straße"
  "Dž".toUpperCase(Locale.ROOT) = "DŽ"(长度 1 → 1);toLowerCase(ROOT) = "dž"(长度 1);toLowerCase(tr) = "dž"
  "I".toUpperCase(Locale.ROOT) = "I"(长度 1 → 1);toLowerCase(ROOT) = "i"(长度 1);toLowerCase(tr) = "ı"
  "i".toUpperCase(Locale.ROOT) = "I"(长度 1 → 1);toLowerCase(ROOT) = "i"(长度 1);toLowerCase(tr) = "i"
  "İ".toUpperCase(Locale.ROOT) = "İ"(长度 1 → 1);toLowerCase(ROOT) = "i̇"(长度 2);toLowerCase(tr) = "i"
  "title".toUpperCase(tr) = "TİTLE",toUpperCase(ROOT) = "TITLE"("i" 在土耳其语里变成带点的 İ)
  "ß".equalsIgnoreCase("SS") = false,"ß".equalsIgnoreCase("ẞ") = true

分步演示

Unicode 与 Charset
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK(Temurin 17.0.20、Homebrew 21.0.12 与 27,每篇注明实际用的版本);用 javac 编译、java 直接运行,需要第三方库时用 Maven 解析依赖;计时用 System.nanoTime,数字每次运行会有小幅抖动。JDK 17.0.20、21.0.12、27;LC_ALL 通过环境变量设置
    • 没有验证:Windows 上的默认编码与控制台代码页、JDK 18 之前 -Dfile.encoding 的所有取值、JDK 21 的 -Dfile.encoding=GBK 为什么不生效(只记录了现象)、stdout 在 GBK 终端的实际字节、BreakIterator 对所有 emoji 序列的覆盖
    京ICP备2026067502号-1京公网安备11010502063487号