Java中表情🤣的String.length()与substring()不符合预期问题
Java中表情字符串substring异常问题解析
问题背景
尝试打印UTF-8字符串前30个字符时,发现Java的String.substring()返回异常内容。聚焦到表情符号🤣(U+1F923),预期它的String长度为1,且substring()不会截断它,但Java认为其长度为2,截取前1个字符得到“?”。确认🤣的UTF-8编码为0xF0 0x9F 0xA4 0xA3,测试Java 11.0.20.1和19.0.2版本均存在此问题,而Python 3表现符合预期。
Java测试代码
import java.nio.charset.StandardCharsets; public class Foo { public static void main(String[] args){ String str = "🤣"; // 表情"ROLLING ON THE FLOOR LAUGHING"的UTF-8字节 byte[] raw = {(byte)0xf0, (byte)0x9f, (byte)0xa4, (byte)0xa3}; String str2 = new String(raw, StandardCharsets.UTF_8); System.out.println(str.equals(str2)); System.out.println(str.length()); System.out.println(str.substring(0,1)); } }
预期输出
true 1 🤣
实际输出
true 2 ?
Python 3测试结果
$ python3 -c 'print(len("🤣"))' 1 $ python3 -c 'print("🤣"[0])' 🤣
问题原因
Java的String类底层基于UTF-16编码实现,而🤣(U+1F923)属于Unicode增补平面字符(代码点大于U+FFFF),这类字符在UTF-16中需要用**两个16位代码单元(代理对)**表示:
String.length()返回的是UTF-16代码单元的数量,而非Unicode字符(代码点)的数量,因此🤣的length()结果为2;substring(int beginIndex, int endIndex)按UTF-16代码单元截取,截取0到1时只取了代理对的第一个单元,这是无效的单独代理字符,最终显示为“?”。
Python 3的字符串默认按Unicode代码点处理,len()返回字符数,索引访问也按代码点取完整字符,因此表现符合预期。
正确处理方式
如果需要按Unicode字符(代码点)截取字符串,可采用以下两种方法:
方法1:通过codePoints遍历处理
public static String substringByCodePoints(String str, int start, int end) { int[] codePoints = str.codePoints().toArray(); if (end > codePoints.length) end = codePoints.length; return new String(codePoints, start, end - start); }
调用示例:
String str = "🤣"; System.out.println(substringByCodePoints(str, 0, 1)); // 输出🤣
方法2:用Character.offsetByCodePoints计算索引
String str = "🤣"; int endIndex = Character.offsetByCodePoints(str, 0, 1); System.out.println(str.substring(0, endIndex)); // 输出🤣
该方法可根据代码点数量计算对应的UTF-16索引,避免截断代理对。
内容的提问来源于stack exchange,提问作者Peter V. Mørch
相关产品推荐
相关产品推荐

