位运算以及逻辑运算
二进制中的原码、反码、补码
要了解位移运算符我们就要先来了解 原码、反码、补码
有符号数
要了解位移运算符我们就要先来了解 原码、反码、补码对于有符号数而言,符号的正、负机器是无法识别的,但由于正、负 “恰好是两种截然不同的状态,如果用"0’表示“正”,用"1"表示“负",这样符号也被数字化了,并且规定将它放在有效数字的前面,即组成了有符号数。所以,在二进制中使用最高位(第一位)来表示符号,最高位是0,表示正数,最高位是1,表示负数。
10000000 00000000 00000000 01111100
对于有符号数而言的性质:
⑴ 二进制的最高位是符号位:0表示正数,1表示负数
(2) 正数的原码、反码、补码都一样
(3) 负数的反码=它的原码符号位不变,其他位取反(0→1 ;1→0 )
(4) 负数的补码=它的反码+1
(5) 0的反码、补码都是0
(6) 在计算机运算的时候,都是以补码的方式来运算的
有符号数运算案列
正数相加
1.正数相加: 例如:1+1,在计算机运算如下 1的原码为:00000000 00000000 00000000 00000001 反码:00000000 00000000 00000000 00000001 补码:00000000 00000000 00000000 00000001 两数的补码相加:00000000 00000000 00000000 00000010(转为10进制)=2
整数相减
2.整数相减:
例如:1-2,在计算机中运算如下:
在计算机中减运算其实是作为加运算来操作的,所以,1-2=1+(-2)
第一步:获取1的补码 00000000 00000000 00000000 00000001
第二步:获取-2的补码
-2的原码:10000000 00000000 00000000 00000010
-2的反码:11111111 11111111 11111111 11111101
-2的补码:11111111 11111111 11111111 11111110
第三步:1的补码与-2的补码相加:
00000000 00000000 00000000 00000001
+11111111 11111111 11111111 11111110
=11111111 11111111 11111111 11111111
第四步:将计算结果的补码转换为原码,反其道而行之即可(如果想将二进制转换为十进制,必须得到二进制的原码)
补码:11111111 11111111 11111111 11111111
反码:11111111 11111111 11111111 11111110(负数的补码=它的反码+1)
原码:10000000 00000000 00000000 00000001(负数的反码=它的原码符号位不变,其他位取反(0→1 ;1→0)
第五步:将计算结果的二进制原码转换为十进制
二进制源码:10000000 00000000 00000000 00000001 = -1
无符号数
无符号数是针对二进制来讲的,无符号数的表数范围是非负数。全部二进制均代表数值(所有位都用于表示数的大小),没有符号位。即第一个"0"或"1"不表示正负。
10000000 00000000 00000000 01111100
比如说我们 java 中 char 它就算是一个无符号数,它占16位,而我们的无符号数 是不能进行运算的。
<< 左移运算符
左移一位
左移一位后的数值经过计算可以发现刚好值位移前数值的两倍,等价于乘2操作,在很多情况下可以当做乘2使用,但是并不代表真正的乘2,在一些特殊情况下并不等价。
左移18位
此时二进制表达首位为1,此时数值为-1058799616,同理,如果继续位移,左位移20位,则值为59768832又变成了正数。
注意: 所以根据这个规则,如果任意一个十进制的数左位移32位,右边补位32个0,十进制岂不是都是0了?当然不是!当int类型的数据进行左移的时候,当左移的位数大于等于32位的时候,位数会先求余数,然后再进行左移我们求得的余数位,也就是说如果真的左移32位的时候,会先进行位数求余数,即为左移32位相当于左移0位,所以左移33的值和左移一位1是一样的。
>>右移运算符(带符号的)
100带符号右移
100带符号右移 100原码补码均为:00000000 00000000 00000000 01100100 右移四位:00000000 00000000 00000000 00000110 结果为:6
-100带符号右移
-100带符号右移 -100原码:00000000 00000000 00000000 01100100 -100补码:保证符号位不变,其余位置取反并加1 11111111 11111111 11111111 10011100 右移4位:在高位补1 11111111 11111111 11111111 11111001 补码形式的移位完成后,结果不是移位后的结果,还需要进行变换才行。其方法如下: 保留符号位,然后按位取反:10000000 00000000 00000000 00000110 然后加1,即为所求数的原码:10000000 00000000 00000000 00000111 结果为:-7
>>> 无符号右移运算符
>>> 无符号右移运算符 无符号右移运算符何右移运算符是一样的,不过无符号右移运算符在右移的时候是补0的,而右移运算符是补符号位的 100无符号右移4位 100原码补码均为:00000000 00000000 00000000 01100100 右移四位:00000000 00000000 00000000 00000110 结果为:6 -100无符号右移4位 -100原码:10000000 00000000 00000000 01100100 -100补码:保证符号位不变,其余位置取反并加1 11111111 11111111 11111111 10011100 无符号右移4位:在高位补0 00001111 11111111 11111111 11111001 结果为:268435449
总结: 正数的左移与右移,负数的无符号右移,就是相应的补码移位所得,在高位补0即可负数的右移,就是补码高位补1,然后按位取反加1即可
逻辑运算
掌握逻辑和运算的区别是︰将二进制数表示的信息作为四则运算的数值来处理就是算数,像图形那样,将数值处理为单纯的0和1的罗列就是逻辑。
计算机能够处理的运算,大体可分为逻辑运算和算数运算,算数运算指的是加减乘除四则运算;逻辑运算指的是对二进制各个数位的0和1分别进行处理的运算,包括(~)、(&)、(丨)和(^)四种。
名称 | 运算符 |
逻辑非 | ~ |
逻辑与 | & |
逻辑或 | | |
逻辑异或 | ^ |
逻辑非指的是将0变成1,1变成0的取反操作
~ 1111 = 0000
逻辑与指的是"两个都是1时,运算结果才是1,其他情况下是0"
0000 0011 & 0000 0101 ------------------- = 0000 0001
逻辑或指的是"至少有一方是1时,运算结果为1,其他情况下运算结果都是0"
0000 0011 | 0000 0101 ------------------- = 0000 0111
逻辑异或指的是"其中一方是1,另一方是0时运算结果才是1,其他情况下是0"
0000 0011 ^ 0000 0101 ------------------- = 0000 0110
我们看完前面基本的运算过后,在java的底层实现中我们知道有很多地方都是用的位运算和逻辑运算,原因应该大家都知道就是效率高,在计算机中不管是加减乘除最后做的都是加法运算,我现在给大家上主菜,然大家来看一下这种运算的巧妙。
使用实例
HashMap大家都不陌生,我们看一下HashMap 是如何初始化容量的。
static final int tableSizeFor(int cap) { int n = cap - 1;//这是为了处理 cap 本身就是 2 的N次方的情况 n |= n >>> 1; n |= n >>> 2; n |= n >>> 4; n |= n >>> 8; n |= n >>> 16; return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; }
就是上述代码我们可以看到用到了>>>( 无符号右移) 和 |(逻辑或) 进行运算。
我们假设我们去传一个参数7给HashMap,我们通过计算,来看它到底会给我们new一个多大容量的数组。
n=7然后再走三元表达式我们就可以得到我们的初始化容量=n+1=8
我们可以通过上面的图片看出最终结果是8,相信你应该看出来,这5个公式会通过最高位的1,拿到2个1、4个1、8个1、16个1、32个1。当然,有多少个1,取决于我们的入参有多大,但我们肯定的是经过这5个计算,得到的值是一个低位全是1的值,最后返回的时候 +1,则会得到1个比n 大的 2 的N次幂。
当然这里为什么这里是2的N次幂,我个人觉得有下面三点原因:
① 为什么不建议初始容量为奇数 ?
我们会经过tab[i = (n - 1) & hash]得到索引,因为如果为奇数,经过n - 1后为偶数,而偶数对应的二进制最低位(末尾)一定为0,在hash进行&运算时,结果值的最低位也一定是0;相比起结果可0可1的情况不仅仅是浪费了一半空间,而且也增加了哈希冲突的概率。
②为什么建议初始容量为2的幂次方而不仅仅是2的倍数?
1.计算索引时下标计算效率更快,对于2的次幂满足 hash & (n - 1) = hash % n,即可以直接取模
2.扩容更快,因为如果n为2的次幂时扩容,每次扩为原数组的 2 倍,假设 n = 32,对应的二进制则为00010000,扩容只需要将1直接往左移 1 位,但是如果 n = 10,对应二进制位00001010 ,不止一个1,所以n为2的次幂时扩容时效率更高。
参考:https://blog.csdn.net/weixin_44742328/article/details/121254561