轻量级前端框架助力开发者提升项目效率与性能
906
2022-11-15
事物的正反两面被哲学家讨论了几千年。计算机里的0和1也照旧玩出了各种花样。...
事物的正反两面被哲学家讨论了几千年。计算机里的0和1也照旧玩出了各种花样。
二进制数 VS 十进制数
本小节讲二进制写法,以及到十进制的转换方法,如果已熟悉这些内容可以直接跳到下一小节。我们生活在一个十进制的世界中。10个一毛就是一块,10个一两就是一斤。在数学上有满十进一或借一当十。十进制数的基数就是0到9,因此所有的十进制数都是由0到9这九个数字组合出来的。计算机底层处理的都是二进制数,可以对比十进制数来看看二进制数的特点:满二进一或借一当二,基数是0和1,就是说所有的二进制数都是由0和1这两个数字组合出来的。就十进制而言,十个1已经达到“满十”的条件,所以要“进一”,于是就是10,这是个十进制数,它的值就是十,因为是十个1合在了一起。就二进制而言,两个1已经达到“满二”的条件,所以要“进一”,于是就是10,这是个二进制数,它的值就是二,因为是两个1合在了一起。如果刚刚这个明白了,结合十进制和二进制的特点,接下来就非常容易理解了:1 + 1 = 2 -> 10。1 + 1 + 1 = 3 = 2 + 1 -> 10 + 1 -> 11。1 + 1 + 1 + 1 = 4 = 3 + 1 -> 11 + 1 -> 100。照此类推,列出几个十进制和对应的二进制:0 -> 0001 -> 0012 -> 0103 -> 0114 -> 1005 -> 101接下来尝试找出二进制和十进制之间的换算关系。首先,十进制数是怎么使用每个位置上的数字表示出来的呢?相信所有人都很熟悉。如下面示例:123 -> 100 + 20 + 3123 -> 1 * 100 + 2 * 10 + 3 * 1因十进制满十进一,要想办法和十联系起来,100就是10的2次方,10是10的1次方,1是10的0次方,于是:123 -> 1 * 10 ^ 2 + 2 * 10 ^ 1 + 3 * 10 ^ 0;进而,我们发现百位的位置是3,但次方却是2,正好是3减去1,十位的位置是2,但次方是1,正好是2减去1,个位就是1减去1,也就是0次方了。于是,这个公式就出来了,太简单了,大家都知道,就不写了。然后,我们把这个“套路”搬到二进制数里试试看吧,只不过二进制数是满二进一,因此要用2的次方。000 -> 0 * 2 ^ 2 + 0 * 2 ^ 1 + 0 * 2 ^ 0000 -> 0 * 4 + 0 * 2 + 0 * 1 -> 0000 -> 0001 -> 0 * 2 ^ 2 + 0 * 2 ^ 1 + 1 * 2 ^ 0001 -> 0 * 4 + 0 * 2 + 1 * 1 -> 1001 -> 1010 -> 0 * 2 ^ 2 + 1 * 2 ^ 1 + 0 * 2 ^ 0010 -> 0 * 4 + 1 * 2 + 0 * 1 -> 2010 -> 2011 -> 0 * 2 ^ 2 + 1 * 2 ^ 1 + 1 * 2 ^ 0011 -> 0 * 4 + 1 * 2 + 1 * 1 -> 3011 -> 3100 -> 1 * 2 ^ 2 + 0 * 2 ^ 1 + 0 * 2 ^ 0100 -> 1 * 4 + 0 * 2 + 0 * 1 -> 4100 -> 4101 -> 1 * 2 ^ 2 + 0 * 2 ^ 1 + 1 * 2 ^ 0101 -> 1 * 4 + 0 * 2 + 1 * 1 -> 5101 -> 5我们发现算出来的正好都是其对应的十进制数。这是巧合吗?当然不是了。其实:这就是二进制数向十进制数的转化方法。我们也可以模仿数学,推导出个公式来:d = b(n) + b(n - 1) + ... + b(1) + b(0)b(n) = a * 2 ^ n,(a = {0、1},n >= 0)就是把二进制数的每一位转化为十进制数,再加起来即可。
负数的二进制 VS 正数的二进制
二进制的常规操作
这些内容应该都非常熟悉了,瞄一眼即可。位操作与(and):1 & 1 -> 10 & 1 -> 01 & 0 -> 00 & 0 -> 0或(or):0 | 0 -> 00 | 1 -> 11 | 0 -> 11 | 1 -> 1非(not):~0 -> 1~1 -> 0异或(xor):0 ^ 1 -> 11 ^ 0 -> 10 ^ 0 -> 01 ^ 1 -> 0移位操作左移(<<):左边丢弃(符号位照样丢弃),右边补0。移完后,最高位是0为正数,是1为负数。左移一位相当于乘2,二位相当于乘4,以此类推。当左移一个周期时,回到原点。即相当于不移。超过一个周期后,把周期部分除掉,移动剩下的。移动的位数和二进制本身的长度相等时,称为周期。如8位长度的二进制移动8位。右移(>>):右边丢弃,正数左边补0,负数左边补1。右移一位相当于除2,二位相当于除4,以此类推。在四舍五入时,正数选择舍,负数选择入。正数右移从都丢弃完开始往后数值都是0,因为从左边补进来的都是0,直到到达一个周期时,回到原点,即回到原来的数值。相当于不移。负数右移从都丢弃完开始往后数值都是-1,因为从左边补进来的都是1,直到到达一个周期时,回到原点,即回到原来的数值。相当于不移。超过一个周期后,把周期部分除掉,移动剩下的。无符号右移(>>>):右边丢弃,无论正数还是负数左边都是补0。因此对于正数来说和右移(>>)没有什么差别。对于负数来说会变成正数,就是使用原来的补码形式,丢弃右边后当作正数来计算。为什么没有无符号左移呢?因为左移时,是在右边补0的,而符号位是在最左边的,右边补的东西是影响不到它的。可能有人会想,到达一个周期后,再移动的话不就影响到了嘛,哈哈,在一个周期的时候是会进行归零的。
二进制的伸/缩
以下内容都假定高位字节在前低位字节在后的顺序。伸:如把一个字节伸长为两个字节,则需要填充高位字节。(等于把byte类型赋给short类型)其实就是这个字节原样不动,在它的左边再接上一个字节。此时符号和数值大小都保持不变。正数符号位是0,伸长时高位字节填充0。00000110 -> 00000000,00000110负数符号位是1,伸长时高位字节填充1。11111010 -> 11111111,11111010缩:把两个字节压缩为一个字节,需要截断高位字节。(等于把short类型强制赋给byte类型)其实就是左边字节直接丢弃,右边字节原样不动的保留。此时符号和数值大小都可能发生改变。如果压缩后的字节仍能放得下这个数,则符号和数值大小都保持不变。具体来说就是如果正数的高位字节全是0,同时低位字节的最高位也是0。或负数的高位字节全是1,同时低位字节的最高位也是1。截断高位字节不会对数造成影响。00000000,00001100 -> 0000110011111111,11110011 -> 11110011如果压缩后的字节放不下这个数,则数值大小一定改变。具体说就是如果正数的高位字节不全是0,负数的高位字节不全是1,截断高位字节肯定会对数的大小造成影响。至于符号是否改变取决于原符号位和压缩后的符号位是否一样。例如,压缩后大小发生改变,符号不变的如下:00001000,00000011 压缩为 00000011,还是正数11011111,11111101 压缩为 11111101,还是负数例如,压缩后大小和符号都发生改变的如下:00001000,10000011 压缩为 10000011,正数变负数。11011111,01111101 压缩为 01111101,负数变正数。
整数的序列化和反序列化
一般来说,一个int类型是由四个字节组成的,在序列化时,需要将这四个字节一一拆开,按顺序放入到一个字节数组中。在反序列化时,从字节数组中拿出这四个字节,把它们按顺序接在一起,重新解释为一个int类型的数字,结果应该保持不变。在序列化时,主要用到的就是移位和压缩。首先将要拆出来的字节移到最低位(即最右边),然后强制转换为byte类型即可。假如有一个int类型数字如下:11111001,11001100,10100000,10111001第一步,右移24位并只保留最低八位,byte b3 = (byte)(i >> 24);11111111,11111111,11111111,1111100111111001第二步,右移16位并只保留最低八位,byte b2 = (byte)(i >> 16);11111111,11111111,11111001,1100110011001100第三步,右移8位并只保留最低八位,byte b1 = (byte)(i >> 8);11111111,11111001,11001100,1010000010100000第三步,右移0位并只保留最低八位,byte b0 = (byte)(i >> 0);11111001,11001100,10100000,1011100110111001这样就产生了四个字节,把它们放入字节数组就可以了。byte[] bytes = new byte[]{b3, b2, b1, b0};在反序列化时,主要用到的就是伸长和移位。首先从字节数组中拿出一个字节,将它转换为int类型,然后再处理符号问题,接着再左移到适合位置。第一步:取出第一个字节,11111001然后伸长为int,11111111,11111111,11111111,11111001因为它的符号位就表示了原来整数的符号位,因此不用处理符号,直接左移24位,11111001,00000000,00000000,00000000第二步:取出第二个字节,11001100然后伸长为int,11111111,11111111,11111111,11001100因为它的符号位是处在原来整数的中间位置的,因此它不表示符号而表示数值,需要处理符号位,就是执行一个与操作,如下,上面两行相与得到第三行,11111111,11111111,11111111,1100110000000000,00000000,00000000,1111111100000000,00000000,00000000,11001100接着左移16位00000000,11001100,00000000,00000000第三步,取出第三个字节,10100000然后伸长为int,11111111,11111111,11111111,10100000然后处理符号位,00000000,00000000,00000000,10100000接着左移8位,00000000,00000000,10100000,00000000第四步,取出第四个字节,10111001然后伸长为int,11111111,11111111,11111111,10111001然后处理符号位,00000000,00000000,00000000,10111001接着左移0位,00000000,00000000,00000000,10111001这样四步就产生了四个结果,如下:11111001,00000000,00000000,0000000000000000,11001100,00000000,0000000000000000,00000000,10100000,0000000000000000,00000000,00000000,10111001可以看到四个字节都已经位于自己应该在的位置上了。最后来一个加法操作就可以了,其实或操作也是可以的。i = i4 + i3 + i2 + i0i = i4 | i3 | i2 | i0这样我们就将字节数组中的四个字节合成为一个int类型的数字了。
模拟实现无符号数
无符号数,即最高位不是符号位而是数值位。有一些语言如Java不支持无符号数,所以需要使用有符号数来模拟实现。因为同一个类型作为无符号数时的范围,会大于作为有符号数时的范围,因此会用更长的类型存放短类型的无符号数。如byte类型是一个字节,作为有符号数时范围是-128到127,作为无符号数时范围是0到255,所以至少需要用两个字节的short类型来存放。处理方法很简单,只需两步,伸长和处理符号位。假如有一个字节是,10101011,这是一个byte类型的负数。第一步,伸长,此时变成两个字节了,但还是一个负数11111111,10101011第二步,处理符号,即执行一个与操作11111111,1010101100000000,1111111100000000,10101011这就已经处理完了,由一个字节的负数变成了两个字节的正数。其实就是将原来的字节前面(即左边)接上去一个全0的字节。当byte作为无符号数,取到最大值255时,二进制是这样的00000000,11111111此时也只不过才刚刚使用完低位置。因此使用长类型表示短类型的无符号数,对长类型的字节利用效率最高也就百分之五十了。对于这种情况,在序列化时,其实只需写入低半部分的字节即可。在反序列化时,一是要用长类型来承接,二是所有字节都要处理符号,作为无符号数对待。
https://github.com/coding-new-talking/java-code-demo.git
原作者:编程新说(李新杰)
版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。
发表评论
暂时没有评论,来抢沙发吧~