Java的char是16位的,装不下所有Unicode字符——这篇真实用一个emoji验证代理对(surrogate pair)机制,以及charAt()、substring()这些"看起来安全"的方法,真实是怎么把一个完整字符从中间切断的。
Java的char类型是16位,能表示的Unicode码点范围是有限的——超出这个范围的字符,真实需要用两个char组合表示。
CodePointSurrogateDemo.java String emoji = "😀"; // U+1F600 emoji.length(); emoji.codePointCount(0, emoji.length());
单个emoji字符串 emoji.length() 真实结果: 2(不是1,因为真实用了代理对) emoji.codePointCount(0, emoji.length()) 真实结果: 1(真实的字符个数是1) emoji.charAt(0) 真实是高代理项: \ud83d emoji.charAt(1) 真实是低代理项: \ude00 真实验证高代理项范围: Character.isHighSurrogate(high) = true 真实验证低代理项范围: Character.isLowSurrogate(low) = true
真实结果证明了 😀(U+1F600,属于超出基本多语言平面 BMP 的补充字符)真实需要两个 char 才能表示——这就是 UTF-16 的代理对(surrogate pair)机制:一个高代理项(\ud83d,范围 \uD800-\uDBFF)加一个低代理项(\ude00,范围 \uDC00-\uDFFF)组合表示一个真实的完整字符。length() 返回的真实是 char 的个数(2),不是真实的字符个数(1)——这个差异是几乎所有代理对相关陷阱的根源。
既然length()真实是2,如果按char位置去截取字符串,会不会真的把这两个char拆开?
CodePointSurrogateDemo.java String mixed = "A😀B"; // 真实length()是4 String cutInHalf = mixed.substring(0, 2); // 只取索引0和1:A和高代理项
"A😀B" 真实length(): 4(真实是4:A + 代理对2个char + B) 真实用substring(0,2)只截取到代理对中间: "A?",真实长度: 2 真实截断留下的最后一个char是不完整的高代理项: \ud83d,Character.isHighSurrogate真实结果: true
CodePointSurrogateDemo.java mixed.codePoints().forEach(cp -> ...); int safeEnd = mixed.offsetByCodePoints(0, 2); // 真实跳过2个完整码点
真实用codePoints()正确遍历: 码点: U+41 -> A 码点: U+1F600 -> 😀 码点: U+42 -> B offsetByCodePoints(0, 2) 真实得到的安全边界: 3 真实安全截取: "A😀"(真实没有拆散代理对)
真实结果验证了两个正确工具:codePoints() 真实按完整的Unicode码点(而不是char)遍历字符串,每次真实拿到的都是一个完整、可正确显示的字符,不管它底层占用1个还是2个char。offsetByCodePoints(start, count) 则真实能帮你计算出"跳过count个完整码点之后对应的char索引"是多少——用这个方法算出的边界去做 substring(),真实能保证不会把任何代理对拆散。
每一帧展示的 char 和方法返回值都是真实运行这段代码时的结果,不是手工画的字符表。