接手那个跑了七年的老系统时,我要改的只是一句提示文案:在"当前库存不足"后面加个括号说明。文件是 message_zh.properties,1846 行、326 个 key,满屏都是 \uXXXX,搜"库存"两个字一个都命中不了。我先花二十分钟逐行比对定位到那一行,改的时候又手滑把 \u5de5 少打了一位,编译直接报 illegal escape character。绕这一圈才弄清:\u 开头那些不是乱码,是同一批汉字换了一种写法。
一、码点是编号,\uXXXX 只是它的十六进制写法
Unicode 给每个字符分配一个编号,叫码点,与字节、与用什么语言都无关。\u4e2d 的意思就是"这个字符的码点,十六进制写作 4E2D"。同一个"中"字,可以有好几种长相。
| 汉字 | 码点 | Unicode 转义 | HTML 数字实体 | 码点十进制 |
|---|---|---|---|---|
| 中 | U+4E2D | \u4e2d | 中 | 20013 |
| 文 | U+6587 | \u6587 | 有 | 25991 |
| 工 | U+5DE5 | \u5de5 | 巧 | 24039 |
| 具 | U+5177 | \u5177 | 具 | 20855 |
范围上也值得记两个数:基本多文种平面是 U+0000 到 U+FFFF,一共 65536 个码位,常用符号和汉字大多落在里面;汉字主区段 U+4E00 到 U+9FFF,占 20992 个码位。超出 U+FFFF 的生僻字和 emoji 没法用单个四位转义表示,得用代理对: 是 U+1F600,要写成 \ud83d\ude00 两段。
二、五种"看着都像乱码"的东西,不在同一层
把这几层混起来处理,是损坏数据的最快方式。
| 长相 | 实际是什么 | 属于哪一层 |
|---|---|---|
\u4e2d\u6587 | 字符码点的转义写法 | 源码 / JSON 字符串层 |
中 | 同一个码点,十进制实体 | HTML 标记层 |
%E4%B8%AD | "中"的 UTF-8 三个字节 E4 B8 AD 被百分号化 | URL 传输层 |
D6 D0 | "中"在 GBK 里的两个字节 | 字节编码层 |
䏿–‡ | UTF-8 字节被当成 Latin-1 显示 | 已经错了,是显示事故 |
最后那行是真乱码:字节解释错了,没有任何"还原成中文"的按钮能把它救回来,只能回到源头改解码方式。同理,"锟斤拷"是把替换字符 EF BF BD 再用 GBK 读了一遍的结果。我见过有人拿 decodeURIComponent 去解 %u4E2D,只换来一句 URI malformed——那是早年 escape() 留下的四不像写法,两头都不算。
三、我最后是怎么改那一行的
- 定位。把整个文件的内容用中文Unicode互转还原一版纯中文,在这份还原结果里按"库存"搜索,拿到 key 名,再回到原文件改那一行。原文件一个字符不动,避免整篇往返转换带来看不见的差异。
- 确认形式。properties 这种文件按行读,转义符写单反斜杠
\u5de5;如果这段文字要嵌进 Java 源码字符串,就得写成\\u5de5。少一位或多一位反斜杠,编译器只会甩一句非法转义。 - 生成回去的那一行时,我用"仅转非 ASCII"这一档,英文字母、数字和半角标点原样保留,diff 里只会显示我真正改动的那几个字;全选转换会把整行都变成转义,评审时根本看不出改了哪里。
- 需要输出成页面能直接用的形式时,同一份文本换十进制模式,拿到
中这种实体写法。
百分号那一层我用的是别的东西:URL 编解码处理的是字节序列,%E4%B8%AD 归它管;文本编码转换管全角半角和 ASCII 值。三者不能互相代替,层级不一样。
四、几个可以直接抄走的数字
- 同样一段"中文":2 个字符,UTF-8 下 6 个字节,GBK 下 4 个字节,写成
\u4e2d\u6587是 12 个 ASCII 字符——一个汉字的转义固定占 6 个字符。 - 按字符数算额度的地方最吃亏:签名、短信模板、配置项全转义等于涨 6 倍,一旦在中间截断,留下
\u4e这种半截就是语法错误。 - 看不见的那几个字符:
\u3000是全角空格(12288),\u00a0是不换行空格(160),\u201c/\u201d是中文弯引号(8220 / 8221)。从文档或聊天窗口复制过的文案十有八九带着它们,先还原成中文再查找替换才有意义。 - 反斜杠的层数别记错:properties 里是单反斜杠
\u5de5,进 Java 源码字符串要写\\u5de5,这段文本再进 JSON 时又得多剥一层。每跨一层就多一次转义,出错多半是在中间某层少写了。
五、常见疑问
Q:\u4e2d 里的 4e2d 是什么?
是"中"这个字符的 Unicode 码点,十六进制 4E2D,换成十进制就是 20013,所以 HTML 实体里写作 中。两者指向同一个字符,只是十六进制和十进制的区别。
Q:源码里写转义和直接写中文,运行起来有区别吗?
解析之后完全等价,是同一个字符。区别在人这一侧:转义写法让全文搜索、代码评审和对文案都失效,而且要求文件本身只能安全地容纳 ASCII。
Q:为什么以前 Java 的配置文件非要转义?
PropertyResourceBundle 在 JDK 9 之前固定按 ISO-8859-1 读这类文件,中文必须以 \uXXXX 形式存进去,早年 JDK 还专门配了 native2ascii 这个转换工具(后来废弃)。这解释了我手上那份文件为什么满是转义——它是历史约束留下的写法,不是坏数据。
Q:文案里的 emoji 为什么字数总对不上?
因为超出 BMP 的字符要占两个 UTF-16 代码单元:'\ud83d\ude00'.length 是 2,一个笑脸在按长度校验的代码里被算成两个字。要按码点统计就用 codePointAt 一类写法,源码里也可以写 \u{1f600}(ES6 起支持)。至于 Invalid Unicode escape sequence 那个报错,通常来自被截断的转义,比如只剩 \u4e,凑不满四位十六进制。
Q:几百行一起转会不会改坏?
转换本身是无损的,风险在覆盖原件。我的做法是转出来的那份只用来查、不用来存,实际改动落在原文件的那几行,提交前用 diff 确认行数还是 1846 行、只有目标 key 变了。
那句"当前库存不足(可在设置里调整)"最后加了进去,用的是原来的单反斜杠转义写法,编译过了。回头看,那二十分钟里真正值钱的经验不是"找到了转换工具",而是先分清自己面对的是哪一层:转义和实体是同一个码点的两种写法,还原无损;百分号那层归 URL 管;至于 䏿–‡ 这种字节已经解错的,只能回到读写两端改解码方式,任何"转成中文"都救不回来。