文章目录
  1. 1. 引言
  2. 2. 问题的产生
  3. 3. 表面兄弟:JAVA
  4. 4. 亲兄弟:Python
  5. 5. 总结
  6. 6. 引用

引言

之所以有这个疑问,是上次阅读Java基础书时碰到讲解char类型没有看明白,并且在代码验证过程中错误的理解了代码的意思,导致我对这么个简单问题产生疑惑并且“恶意揣测”Java内部的黑魔法,这里就把我如何走上歪路,并且最终找到“正确”的道路的故事讲出来

问题的产生

我们知道Java是采用Unicode进行内部编码,但是使用UTF-16作为外部编码。

怎么来理解这个东西呢。首先你要知道Unicode是在我们熟悉的GB 18030BIG-5ISO8859-1之后出现的,它的出现就是为了统一全世界的编码,因为前面这些编码都太片面了,只包含自己国家或者少数几个国家的字符。

Unicode的目的就是包括全世界的编码,并且给未来可能出现的编码留下位置,你可以理解为它是一张大“表”,一般我们使用16进制来表达它,并且在前面加上U+。例如U+0041代表字母A,但是这里有个历史问题

一开始我们知道Unicode为了包含全世界的字符从ASCII的一个字节扩展到两个字节,就能包含65536个字符了,但是随着字符包含越来越多,我们逐渐需要更多字符了,最后扩展到U+0000 -> U+10FFFF去了,为了表示这些我们必须使用三个字符,假设我们不考虑内存成本,每个字符都使用四个字符来表示(不使用三个是为了内存对齐),那么问题就解决了,大家都用Unicode来表示,这样我传给你一串字符你就能秒懂了。

但是学过信息论就知道,单字符越长信息熵也就是信息量就少,其实在日常通信中我们并不是每个字符都会用到,为了提高效率,我们可以使用霍夫曼、香农编码技术对信息重新编码,这个就是UTF-8UTF-16等现代编码的理论基础。

这就好比特种部队手势,我们把作战命令(Unicode)需要的指令放到手势(如UTF-8)里面,这样几个手势就能表达复杂的作战计划(假如用嘴巴说的话)。

接下来我们就从JAVA和Python来看,编码与其关系

表面兄弟:JAVA

Unicode对于JAVA来说,只能算是表面兄弟,虽然内部支持Unicode编码,但是其本质还是基于UTF-16编码,为什么要这么说呢。

我们来回顾一下,我们知道Unicode的范围是U+0000-U+10ffff,这意味着我们没法用两个字节来表示,但是在Java里面char类型字节为2字节,而对于字符串类String来说,其组成就是一个char字组,对于小于U+10000Unicode码来说,String对象最小组成单位就是char,但是对于大于U+10000Unicode码来说却是char数组,我们用代码来展示一下两者之间的关系。

char[] chars = Character.toChars(0x1f121);
String s = new String(chars);

而且我们将s输出的话,会发现它是一个字符,但是它的length却为二,而且我们将s每个字符转换成二进制你会发现他们的值依次为0xd83c0xdd21,他们存贮的值全部以UTF-16的格式存贮,具体编码详细我就不细说了,下面资料介绍的很详细(需要翻墙)。在Unicode里面占一个字符的值,却以两个基本类型存贮,当然为了维持这种“表面兄弟”的关系,Java也使用了“码点”来支持一下兄弟,只要使用codePointAt代替charAt,用codePointCount代替length,我们也能处理超过U+10000Unicode编码(对于不超过U+10000的字符那就是“真兄弟”)

当我不知道一个char只能放两个字节的时候,我强行使用char c = (char)0x1f121来“存”一个超过U+10000Unicode码,结果被Java无情的溢出掉,只取到了部分值,但是我却误以为Java有黑魔法能用两个字节存贮了三个字节才能存下的值,乃至我闹了个笑话。

总结一下Java是一个非常严谨的语言,规定死的东西就不会变,表面上看Java能够支持Unicode编码,但是实际上他只是编译器支持,比如你写一个🄡(0x1f122)的值来赋给String如下面:

String ns = "🄡"

表面上看,Java完全支持Unicode码,但在实际的上面他内部还是用UTF-16进行编码,只是在编译的时候帮我们将0x1f122转换成为两个
0xd83c0xdd21存贮在char字符组里面。

其实这个表面兄弟是相对的,从Python3``Unicode支持来比较一下就能发现不同。

亲兄弟:Python

Python3Unicode是非常友好的,它在明面上完全按照Unicode的编码表使用来存贮Unicode码,对应它的Unicode字符串,最小单元都是Unicode码,多说无意,上代码。

  c = chr(0x1f122)
print(len(c))  # = 1
print(type(c)) # str

我们可以看到我们得到的最小的码元是字符串str类型,无论这个Unicode码是否大于U+10000Python都把它视为一个基本单位,这样避免了你对其进行一些误操作,插句话来讲讲怎么得到这个大小呢,我们使用sys.getsizeof方法就能计算出来

sys.getsizeof(chr(0x1f122))  # 80
sys.getsizeof(chr(0x1f122) * 2) # 84

由于Python使用一些字段来标注类型,所以直接使用sys.getsizeof得不得一个Unicode码需要的字节,所以我们计算两个的差,很清楚的就能得到一个Unicode码使用四个字节,你可以依次乘下去,而且你发现一个有趣的现象,对于小于U+007FUnicode码,其大小为一字节,而对于U+0080-U+07FF其大小为两字节。具体可以看参考资料Python内部是使用UTF-8来存贮Unicode码的,但是Python将这一切都隐藏起来,你从表面上看好像一个Unicode就是一个最小单元,对于其底层我们不得而知,我们可以从侧面来验证一下

 timeit.timeit("'中国人'.encode('gbk')")
>> 0.6366317819999949
timeit.timeit("'中国人'.encode('utf-8')")
>> 0.2109854949999317

我们可以看到将Unicode编译成其他编码方式,其中utf-8速度是最快的,因为基本上是复制一下就行了,而其他的差距到了三倍

总结

通过前面我们知道,Python之所以 Unicode如此“亲兄弟”是因为做了一层封装得来的,相比JavaUnicode码(使用UTF-16作为底层编码)暴露给出来,Java在底层上却是非常“坦诚”,你想直接使用Unicode码值也可以,Java编译器会帮你把Unicode码值转换成UTF-16,你也可以从UTF-16码生成String字符串,这样底层在实现查找的时候也是使用统一的编码进行。但是也正是由于这么“底层”,代码看起来总不是那么“亲”,相比于Python的“一视同仁”,我们也可以理解这就是这两种语言的各自特点所在。

总的来说如果你想直接接触代码底层,推荐使用Java,假如你只想研究其本质,推荐使用Python来进行自然语言处理,他的封装能让你不需要了解其内部组成。

引用

https://zh.wikipedia.org/wiki/UTF-16

https://en.wikipedia.org/wiki/UTF-8

文章目录
  1. 1. 引言
  2. 2. 问题的产生
  3. 3. 表面兄弟:JAVA
  4. 4. 亲兄弟:Python
  5. 5. 总结
  6. 6. 引用