浅析码点和代码单元

浅析码点和代码单元

​ 这几天在学习《Java核心技术 卷一》的时候学习到了一个叫“码点和代码单元”的小节,感觉不是十分明白,在多次查阅资料求证之后,对其有了比较基本的正确的了解。

​ 码点和代码单元的概念是基于Unicode编码集的,Unicode编码集是一个由国际标准化组织建立的旨在标准化全球计算机字符编码的大编码集,它收录了人类所创造的几乎所有文字,它规定了每个字符对应的编号,然而它并没有规定这些编号该如此存储,UTF-8,UTF-16这些编码规则,规定的就是字符对应的编号在计算机中如此存储。顾名思义,UTF-8是一次最少传输8位,UTF-16是一次最少传输16位,这个一次的最少传递,就是代码单元了。

代码单元

​ 代码单元就是一个字符的编码在存储时所占据的最小单位,或者说是编码的单位,一个编码会存储为1个、2个、4个代码单元,他们的大小以代码单元为单位进行递增。简单来说,代码单元的含义就是,字符编码的存储单位。如UTF-8的代码单元是一个字节,UTF-16的代码单元为两个字节,UTF-32的代码单元为4个字节。

​ 通常情况下,在我们使用中规中矩的普通字符时,不会耗费太多的代码单元,通常来讲是一个字符占用一个代码单元就好了,如英文字母。然而我们用到其他乱七八糟的字符时,这些字符对应的编码就大起来了,因为有限的字节难以标定更多的字符,如一个字节只有八位,这八位实际上只能描述128个字符,这涉及到UTF-8的编码规则,因此我们需要更多的数位来创造更多的二进制数来标定其他字符,这时就使用到了UTF-8的编码机制,它是使用了变长字节机制,当需要标定更多字符时,它可以增加代码单元,这样用来进行二进制位扩展,表示的字符也就多了,这里涉及UTF-8的编码规则,我参考了sally的jerry-CSDN博客_utf8与unicode的区别这一篇博文,之后我也会在此详细整理这个知识点。总之我们看到,对于UTF-8和UTF-16这种边长字节编码,他们的单个字符的位不是固定的,而是根据代码单元进行扩展的。

码点

​ 码点相对于代码单元来说更加上层,码点指的是:一个字符对应的编码。在编码中,每一个字符都有自己对应的编码,一个字符有一个对应编码,这个编码就是码点,我们使用相对应的函数是可以将这个编码转化成为字符的,当然这种操作必要性不强。

相关方法操作以及注意事项

​ 由上可知码点和代码单元的意义和区别,乍一看好像没什么用,但是在实际操作中,Java为我们书写了针对码点以及针对代码单元的相关操作,我们在使用时需要真正的了解码点和代码单元的区别,以及要趋利避害的使用他们。

char charAt(int index)方法:这个方法可以返回字符串中下标为index的代码单元并转化成字符。

int offsetByCodePoints(int startIndex, int cpCount)方法:这个方法可以从一个字符串的某个位置为起始,返回往后推cpCount个字符位置的字符的索引。

int codePointAt(int index)方法:返回给定位置的码点

​ 其中int offsetByCodePoints(int startIndex, int cpCount)这个方法大有脱裤子放屁的画蛇添足之第一印象,为啥我输入一个我知道的下标,它还要返回一个下标?事实这样的,在这三个函数中,或者说在Java中,关于字符串字符数位的下标数字,它们的计量单位都是代码单元,也就是说,在三个函数中,输入的下标参数也好,输出的下标结果也好,它们都是以指定字符编码方式的代码单元为单位进行计数的,因此在遇到多代码单元表示的字符时,大概率出现错误。我们先从最基本的charAt开始看。

public class MyTry {

	public static void main(String[] args) {
		String str = "这𝕆是一个𝕆!";
		System.out.println(str.charAt(1));
	}

}

​ 输出结果为“?”,这是因为“𝕆”字符是一个辅助类型字符,它是由多个代码单元构成的,charAt方法是取得相应下标的代码单元并转化成字符,当取到“1”位置的代码单元时,它正好取得的是“𝕆”字符的第一个代码单元,这个代码单元没用对应的字符的,同理,单独取位置“2”的代码单元,也是无法解析的。

​ 使用charAt盲目取字符还有另一个弊端,就是因为辅助字符的存在,输入结果会与预期结果发生错位,如:

public class MyTry {

	public static void main(String[] args) {
		String str = "这𝕆是一个𝕆!";
		System.out.println(str.charAt(4));
	}

}

​ 我们输入了一个4,希望取得字符串中从0开始数下标为4的字符,也就是第五个字符,但是这个方法在运行时,其计数器在经过“𝕆”字符时会额外增加一次,也就是说在第四个字符“一”位置的时候,它的计数器就已经达到5了,因为它是按照代码单元来进行增加的,因此,结果是“一”,而不是“个”。

​ int offsetByCodePoints(int startIndex, int cpCount)方法的说明中提到了它可以返回往后推移cpCount个字符的位置,也就是说,给定它一个位置,它可以以字符为单位往后推,这么一听,这家伙相当只能,实际上不是,我们仍然需要小心的使用,因为它的startIndex参数,仍然是以代码单元为单位推进的。如下:

public class MyTry {

	public static void main(String[] args) {
		String str = "这𝕆是一个𝕆!针不戳";
		int i = str.offsetByCodePoints(2, 5);
		int cp = str.codePointAt(i);
		char[] chars = Character.toChars(cp);
		String str1 = new String(chars);
		System.out.println(str1);
	}

}

​ 其输出结果是“一”,我们是使用下标2作为开头的,在我们看来,下标为5的字符是“针”,往后推移,确实可以识别“𝕆”字符了,但是仍然发生了与预期相违背的错位,真正的输出结果是“!”,这是因为它的入参startIndex仍然是以代码单元为单位的,如果我们按照自己的单位习惯输入,就会导致前面的我们所认为的开始下标比实际的开始下标少,进而产生错位。然而仍然有安全的使用方法,当我们的入参startPoint为0时,那么问题就都解决了,因为从0开始,可以绝对保证前面没有字符,0这个起点是绝对准确的,无论用字符为单位还是用代码单元为单位,这个起点的下标都是0,这样一来,它可以准确的返回一个码点的下标。

​ int codePointAt(int index)方法则可以返回指定位置的码点,这样结合两个方法,我们就可以获得一个以字符为单位的第n个字符的码点值了,然后再使用码点转换为char类型数组,char类型数组转化为String类型的方式,获得我们想要位置的字符了。

public class MyTry {

	public static void main(String[] args) {
		String str = "这𝕆是一个𝕆!针不戳";
		int i = str.offsetByCodePoints(0, 5);
		int cp = str.codePointAt(i);
		char[] chars = Character.toChars(cp);
		String str1 = new String(chars);
		System.out.println(str1);
	}

}

​ 这个输出结果是𝕆,就是准确的第六个位置的字符。

总结

​ 码点指的是一个字符的具体编码值,而代码单元则是Unicode编码中的最小单位,在Unicode中,一个码点可能由一个代码单元构成,可能由两个代码单元构成,具体由多少个代码单元构成一个码点还得看具体的编码方式。当我们使用charAt方法时,取出的其实是代码单元,使用codePointAt方法时,是按照具体地址信息取出的一个字符的码点值,二者的入参均为int类型,这个int类型实际上是以代码单元为单位移动的下标,因此我们在使用codePointAt时也要注意,我们一定要传入正确的地址,我们可以使用offsetByCodePoints方法获取这个正确的下标。

​ 这里是对于码点和代码单元概念的大体解析,基本上明确了码点和代码单元这两个概念,在之后,我打算详细的研究一下编码这门学问,因此在之后还会有新的笔记进行更加深化的研究。

posted @ 2022-01-13 08:30  云杉木屋  阅读(587)  评论(0编辑  收藏  举报