最近在开发输入法程序时遇到一个小问题,就是删除一个emoji时,不能一次删干净,需要执行两次操作才可以。Intuitively,这肯定是java操作unicode字符的问题,于是找了JAVA官方文档参考一下,解决了这个问题,这里做下简单总结。原文在这里,有兴趣自己看。
http://www.oracle.com/technetwork/articles/java/supplementary-142654.html
注:文章中提到的“JAVA字节”均指JAVA平台的16位字节,请不要和C的8位字节搞混。
首先需要知道标准Unicode字符是一个16位字符(废话),所以标准Unicode字符集包含65536个字符(2的16次方嘛,哥的数学很好的)。但是仅仅65536个字符是远远不够用的,尤其是为了包含我们伟大中华民族丰富多彩的文字(反正我是不认识),Unicode字符集进行了扩展,扩展到了24位,也就是最多可以包含1,112,064个字符。这里我们把标准Unicode字符集(也就是前65536个字符)叫做Basic Multilingual Plane (BMP),把超过16位以上的扩展字符集叫做supplementary characters。
UTF-16是一种编码方式,以16位无符号单元来编码Unicode字符,如果对一个标准Unicode字符编码,只需要占用一个UTF-16单元,如果对扩展Unicode字符编码则需要占用两个UTF-16单元。
我们都知道在C语言中,一个primitive char占用一个字节,也就是8位。但是在JAVA中,一个primitive char占用16位,与一个标准Unicode字符长度相等,因为JAVA平台采用UTF-16进行编码。这样JAVA就很容易处理Unicode基本字符。但是对于Unicode的扩展字符,在JAVA中就需要占用两个char,也就是两个UTF-16单元。
举个栗子,大写字母A的Unicode值为U+0041,它属于Unicode的基本字符集,所以它只占用一个UTF-16单元,表示为[0041]。而字符的Unicode值为U+10400,它属于扩展Unicode字符集,所以它占用两个UTF-16编码单元,表示为[D801][DC00](一个中括号代表一个UTF-16单元,中括号本身没意义)。第一个单元叫做high-surrogates,范围从 U+D800 到 U+DBFF,第二个单元叫low-surrogates,范围从 U+DC00 到 U+DFFF,这个看起来很类似多字节编码。但是,我说的是但是,这里有一个非常重要的不同点,从U+D800 到 U+DFFF 其实为UTF-16的保留值范围,专门用作编码Unicode扩展字符集,这个范围不被赋予任何实际的标准Unicode字符。也就是说,在你的程序里只要判断一个字符是否属于这个范围内,就可以知道这个字符是应该被当做一个独立的Unicode基本字符处理,还是当做半个扩展Unicode字符。
回到我一开始提到问题,我在开发android输入法的时候,当需要删除一个emoji时,总是需要删除两次才能删干净。这是因为我用的emoji都是属于Unicode扩展字符集的,在编辑框中占用了两个UTF-16单元,而每执行一次删除操作只删除了一个UTF-16单元,也就是一个JAVA字节,而另一个单元没有被删除,所以就会显示异常。这时候只要再删除掉另一个字节就算真正把这个emoji删除干净了。
用户在实际的应用中,肯定是把Unicode标准字符集和扩展字符集混合着使用,也就是说有的字符占用1个java字节,有的字符占用两个java字节。那么我在执行删除一个字符的操作时必须也要先去判断我是要一次性删除一个字节还是删除两个字节。这就很简单了,按照我前面提到的规则,先判断一下被操作的字符值是否处于[U+D800,U+DFFF](inclusive)之间,如果是,就说明被操作的字符不是一个有效的标准Unicode字符,而是半个Unicode扩展字符,那么只需要在程序中自动删除两个java字节就可以了。反之,如果被处理的字符不属于[U+D800,U+DFFF]范围内,那么这个字符就是一个标准的Unicode字符,只需要按照一个java字节处理就可以了。
理论清楚后(我假设你清楚了,其实也不难,就是我表达的不清楚,没办法,工科人,总是思维超越语言),我再简单介绍一下java中相关的处理函数。
Character.toChars(int codePoint):参数codePoint为Unicode值,此函数将Unicode值转换为标准java字节数组。如果codePoint是Unicode标准字符,则返回值只包含一个char;如果codePoint是扩展Unicode字符,则返回值包含2个char。
例:在程序中如果要输出一个Unicode扩展字符,可以这样String.valueOf(Character.toChars(0x1F60E))
Character.isLowSurrogate(char ch):判断一个字符是否是一个Unicode扩展字符的低16位编码。
Character.isHighSurrogate(char ch):判断一个字符是否是一个Unicode扩展字符的高16位编码。