小bit最近在敲代码的时候,碰到了一个奇怪的问题。一个表情复制给char竟然报编译,难道是被我发现了java的重大漏洞吗?提交漏洞会不会出名了?是不是可以走上人生巅峰了?赶紧上网上查查。
char ch = '😁'; // 编译错误
一查才发现,这个是一个已知的问题。Java中的char使用的是2字节的UTF-16的编码方式,不能表示这种emoji表情。为什么呢?我们先来看下UTF-16是如何编码unicode的。
Unicode最多可以1114112个字符,数字0-0x10FFFF来映射这些字符。这些字符被分配到了17个平面,一个基本多文种平面BMP,基本平面对应的是0 ~ 0xFFFF,包含了世界上正在使用的常用字符,因此,平常使用的字符一般都位于BMP平面上。16个辅助平面SP,留作扩展之用或用来表示一些特殊的字符。BMP里存在一种特殊区域: 代理区(Surrogate)。Unicode标准规定U+D800 - U+DFFF的值不对应于任何字符。后面可以看到,UTF-16就巧妙地利用了这一段空白区域进行了编码的转换。
现在我们来看下UTF-16是怎么进行编码的,如果是字符在基本平面的,UTF-16就使用两个字节来标识,对应的值就是字符对应的码点。如果是辅助平面的字符,两个字节已经无法表示,UTF-16使用四个字节来标识这部分的字符。这就就需要用到BMP中的代理区。如果一个辅助平面的字符,则对应的码点先减去 0x10000,因为辅助平面字符的范围在0x10000 ~ 0x10FFFF,这样换算后范围就变为了 0 ~ 0xFFFFF,对应的二进制为yyyyyyyyyyxxxxxxxxxx,然后把前面10位固定拼接110110,110110yyyyyyyyyy,使用两个字节表示,作为高位代理,后面10位拼接110111,使用两个字节来标识低位代理,通过高位代理和低位代理,使用4个字节来表示一个辅助平面的字符。因为高位代理和低位代理都是在BMP的代理区,所以不会和其他BMP中的正常字符弄混。
下面我们看一个例子。😁,对应的UNICODE码点是 128513,对应16进制是0x1f601,减去0x10000后为 0xf601,对应的2进制是1111011000000001,分割为高位代理是1101100000111101 对应是 0xD83D, 低位代理是 1101111000000001 对应16进制是 0xDE01,所以对应的UTF-16编码是\uD83D\uDE01 。
上面介绍了这么多,我们回来再看下我们今天碰到的问题。Java中的char为啥不能表示笑脸呢?因为char是两个字节的,只能表示UNICDOE中的BMP,但是笑脸不在BMP,需要4个字节来表示,所以才会报错的。
Java中提供了很多对对辅助字符的支持。
//利用codePoint直接构造String
String s1 = new String(new int[]{128513 } , 0 , 1);
System.out.println("s1 : " + s1);
//输出 s1 : 😁
//利用Character.toChars返回对应的char数组,从而构造string
String s2 = new String(Character.toChars(128513));
System.out.println("s2 : " + s2);
//输出 s2 : 😁
String s3 = "😁123中文😁";
System.out.println(s3.length());
//输出 9 ,对应的是string中char数组的长度
System.out.println(s3.codePointCount(0 , s3.length()));
//输出7, 对应的是string中unicode字符的个数
int codePoint1 = s3.codePointAt(0);
System.out.println("codePoint1 : " + codePoint1);
//输出是 codePoint1 : 128513,对应的是 😁 的unicode码点
int codePoint2 = s3.codePointAt(2);
System.out.println("codePoint2 : " + codePoint2);
//输出是 codePoint2 : 49,对应的是 1 的unicode码点
System.out.println(Character.isBmpCodePoint(codePoint1));
//输出false,因为 码点 128513 不属于基本平面BMP的
System.out.println(Character.isSupplementaryCodePoint(codePoint1));
//输出true, 因为 码点 128513 属于辅助平面SP的
System.out.println(Character.isBmpCodePoint(codePoint2));
//输出true 因为 码点 49 属于基本平面BMP的
System.out.println(Character.isSupplementaryCodePoint(codePoint2));
//输出false , 因为 码点 49 不属于辅助平面BMP的
System.out.println(Integer.toHexString((int)Character.highSurrogate(codePoint1)));
//d83d,对应我们在文章上面算出来的高位代理
System.out.println(Integer.toHexString((int)Character.lowSurrogate(codePoint1)));
//de01,对应我们在文章上面算出来的低位代理