前言

最近为了加强基础,再看《Java核心技术 卷I》,3.3.4 节Unicode和char类型中有一段专业术语,来解释Java语言从Java 5开始如何解决这个问题。本人稍作研究,先附上书中的内容:

码点(code point) 是指与一个编码表中的某个字符对应的代码值。在Unicode标准中,码点采用十六进制书写,并加上前缀U+,例如 U+0041就是拉丁字母A的码点。Unicode的码点可以分成17个代码平面(code plane)。第一个代码平面称为基本多语言平面(basic multilingual plane),包括码点从 U+0000到 U+FFFF的 “经典” Unicode代码;其余的16个平面的码点为从 U+10000 到 U+10FFFF,包括辅助字符(supplementary character)。


UTF-16编码采用不同长度的编码表示所有Unicode码点。在基本多语言平面中,每个字符用16位表示,通常称为代码单元 (code unit);而辅助字符编码为一堆连续的代码单元。采用这种编码对表示的各个值落入基本多语言平面未用的2048个值范围内,通常称为替代区域(surrogate area)(U+D800~U+DBFF用于第一个代码单元,U+DC00~U+DFFF用于第二个代码单元)。这样设计十分巧妙,我们可以从中迅速知道一个代码单元是一个字符的编码,还是一个辅助字符的第一或第二部分。例如,𝕆是八元数集(http://math.ucr.edu/home/baez/octonions)的一个数学符号,码点为U+1D546,编码为两个代码单元U+D835 和U+DD46。(关于编码算法的具体描述见https://tools.ietf.org/html/rfc2781。)


在Java中,char类型描述了UTF-16编码中的一个代码单元

分析

根据书中的描述,U+000000到U+00FFFF共16位表示“经典”Unicode代码,其余的U+010000到U+10FFFF代表其他。我们来分析一下:


000000 00FFFF “经典”Unicode代码
010000 01FFFF 1号辅助字符集
020000 02FFFF 2号辅助字符集
… … …
0F0000 0FFFFF 15号辅助字符集
100000 10FFFF 16号辅助字符集

也就是说我们需要多少位(bit)来存储这17个代码平面呢?
其实就是 21+20,也就是21bit。

Java在1.5之前的字符都是以16bit来存储的,突然多了很多字符要变成21bit,如何才能做到即兼容之前版本的字符,又能够让它用16bit来存放21bit的数据呢?

首先我们要确定肯定正确的事:如果想让16bit存放21bit的数据,是不可能的。只能通过多来几bit才能存放21bit的数据。Java设计了一个十分巧妙的方法:

方法

前提:在“经典“的 216 个字符中,留了许多并没有定义,也就是很多没有使用到的码点。

为了节省空间,并保留兼容性,由于原先的Unicode编码中有很多码点并没有定义,从中拿出2048个码点(U+D800~U+DFFF,一共是2048个码点)。

将这2048对半分一下分为前面一个1024的部分(high)和后面一个1024的部分(low)。



Java怎么实现点名系统 java代码点_character



由于1024刚好是2

10 如果我们将high作为高位,low作为低位,理论上就形成了一个可以存储20位数据的结构。

如果还是如果还是不懂的话,就让high左移10位,作为高10位,low不移,作为低10位,这样就组成了一个新的结构啦。

实现

讲了这么多,先看成果:

// 输出原有的Unicode码点所代表的字符
System.out.println("\u0026"); //&
// 输出 high 和 low 组合成的字符
System.out.println("\uD83E\uDDC5");    //是个小灯笼🧅字符,而并非是两个字符

其实如果要实现的话,理论上也不难:

int codePoint = 当前码点;
if (codePoint > 0xD7FF && codePoint < 0xDC00) { //是高位
	if(有下一个码点){
		int high = codePoint; //这里写只是方便理解,实际直接使用codePoint即可
		int low = 下一个码点;
		
		return 去其余代码平面的规定里查找该码点代表的字符; //这里通过(high<<10) && low去查找码点代表的字符
	}else return '?';	//没有低位,则表示这个码点是错误的,返回 '?'
} else if(codePoint > 0xDBFF && codePoint < DE00){ //是低位
	return '?'; //没有输入高位而直接输入了低位,出错,返回 '?'
} /*else if(...){} 这里存放其他规定的判断*/
return 去Unicode的规定里查找该码点代表的字符;

可能实现上有不同,但个人认为核心思想应该是这样,若有误欢迎指出(将 codePoint 定义为int类型是为了方便计算,int是32bit,而实际我们只需要16bit)。





小思考

提问:就算可以通过组合的方式也只能表示20位数据也就是16种代码平面(U+00000~U+FFFFF,20bit)呀,为什么实际能保存17种(U+00000~U+1FFFFF,21bit)呢?

答:实际上最后那一种就是Unicode(U+00000~U+0FFFF去除掉中间占用的部分,因为这些码点并没有被定义),Java是从Unicode种抽出了2048个没有用到的码点来保存其余的16种代码平面。

也就是在U+0000~U+FFFF中抽出了2048个从未使用到的(保留的)码点,如果程序在遇到这2048个码点的时候就明白了:哦,这是一个两个char(代码单元)拼接成的一个字符。通过判断其代表的高位和低位再去查表即可得知该两个代码的组合代表的是哪个字符。


个人猜测

当然,Java在当时应该是先看到了Uniode编码具有保留字才想到的这一解决方案,既可以兼容以前的Unicode编码,还可以扩充编码范围,并且程序在处理起来还是较为容易的。对于大多数的字符来说,只需要1个16bit就可以表示,只有用到极少次数的那些字符才需要2个16bit来传输,相比使用21bit或24、32bit来存字符的方法而言大大减少了传输时所用的时间和用来保存的空间。

可见,在一开始的时候设计好一个东西对后期的影响是非常显著的,如果Unicode当时设计的并不好,可能它只能够通过21、24、32bit来存放字符,或者是使用其他的方法,当然这种方法应该会比现在的这种处理方式要麻烦的很多,并且效率也不一定比现在的处理方式高。