文章目录
- Java中支持的位运算
- 位运算规则
- 逻辑运算
- 与运算(&)
- 或运算(|)
- 异或运算(^)
- 取反运算(~)
- 位移操作
- 左移(<<)
- 右移(>>)
- 无符号右移(>>>)
- 位运算的使用奇巧淫技
- 判断奇偶数
- 获取二进制位是1还是0
- 交换两个整数变量的值
- 不用判断语句,求整数的绝对值
- 常见面试题
Java中支持的位运算
- 位与(&):二元运算符,两个为1时结果为1,否则为0
- 位或(|):二元运算符,两个其中有一个为1时结果就为1,否则为0
- 位异或(^):二元运算符,两个数同时为1或0时结果为1,否则为0
- 位取非(~):一元运算符,取反操作,即1变为0,0变为1
- 左移(<<):一元运算符,按位左移一定的位置。高位溢出,低位补0,符号位不变。
- 右移(>>):一元运算符,按位右移一定的位置。高位补符号位,符号位不变,低位溢出。
- 无符号右移(>>>):一元运算符,高位补零,低位溢出。
位运算规则
计算机对于计算数据的话,都是以二进制来进行运算的,所以使用位运算相比直接使用(+、-、 *、/)运算符,要更高效,能显著调高代码在计算机中的执行效率。
对于有符号的而言,
- 最高位为符号位,0表示正数,1表示负数
- 正数的原码,反码和补码都一样,三码合一
- 负数的反码:符号位保持不限,其他位取反
- 负数的补码:反码 + 1
- 0的反码和补码都是0
- 数据存储和运算都是以补码的方式进行的
下面以 -1 为例子展示原码、反码和补码的转换关系(以int数据类型为例,int类型在Java中占4字节):
逻辑运算
与运算(&)
两个数相同位置的比特进行与运算,若两个位置均为1,那么结果就为1,否则为0。
计算 a = 4 & -5
因为4为正数,所以原码和补码相同,即4的补码为:00000000 0000000 00000000 00000100
因为-5为负数,所以需要进行原码 -> 反码 -> 补码的转换。转换步骤如下:
1)原码:10000000 00000000 00000000 00000101
2)反码:11111111 11111111 11111111 11111010
3)补码:11111111 11111111 11111111 11111011
将4和-5的补码进行 & 运算得到补码结果:00000000 0000000 00000000 00000000
所以最终结果:a = 0(0的反码和补码都是0)
或运算(|)
两个数相同位置的比特进行或运算,若两个位置其中一个为1则结果为1,否则为0。
计算 a = -2 | 5
因为-2为负数,所以需要进行原码 -> 反码 -> 补码的转换。转换步骤如下:
1)原码:10000000 00000000 00000000 00000010
2)反码:11111111 11111111 11111111 11111101
3)补码:11111111 11111111 11111111 11111110
因为5为正数,补码和反码一致,所以5的补码为:00000000 00000000 00000000 00000101
将-2 和 5 的补码进行 | 运算得到补码结果:11111111 11111111 11111111 11111111
结果显然是一个负数,而负数的补码和原码不一致,所以需要将补码结果转换为原码才能得到最终的结果
补码转原码的过程是跟原码转补码相反的过程,具体过程如下:
1)补码:11111111 11111111 11111111 11111111
2)补码 -1 得到反码:11111111 11111111 11111111 11111110
3)符号位不变,其他位置取反得原码:10000000 00000000 00000000 00000001
所以最终结果:a = -1
异或运算(^)
两个数相同位置的比特进行异或运算,若两个数均为0或1,则结果为0,否则为1。
计算 a = 1 ^ -5
1的补码为:00000000 00000000 00000000 00000001
-5的补码为:11111111 11111111 11111111 11111011
两个补码进行 ^ 运算得到补码结果:11111111 11111111 11111111 11111010
补码为负数,需要转换成原码:
1)补码:11111111 11111111 11111111 11111010
2)反码:11111111 11111111 11111111 11111001
3)原码:10000000 00000000 00000000 00000110
所以最终结果:a = -6
取反运算(~)
若位数为0,则取反后为1,若为1,取反后为0。
计算 a = ~2
2的原码为:00000000 00000000 00000000 00000010
2的补码为:00000000 00000000 00000000 00000010
取反:11111111 11111111 11111111 11111101
取反后的结果仍为补码,此时补码为负数,则需要转成原码
-1 得到反码:11111111 11111111 11111111 11111100
符号位不变,其他位置取反得到原码为:10000000 00000000 00000000 00000011
所以最终结果:a = -3
位移操作
Java中的位移操作只针对int类型的有效,Java中,一个int的长度始终是32位,也就是4个字节,它操作的都是该整数的二进制数。也可作用于以下类型,即 byte,short,char,long(它们都是整数形式)。当为这四种类型时,JVM先把它们转换成int类型再进行操作。
左移(<<)
高位溢出,低位补0,符号位不变。移动位数超过该类型的最大位数,则进行取模,如对Integer型左移34位,实际上只移动了两位。左移一位相当于乘以2的一次方,左移n位相当于乘以2的n次方。
正数 a = 20 << 2
20的二进制补码:00000000 00000000 00000000 00010100
向左移动两位后: 00000000 00000000 00000000 01010000
结果: a = 80
负数 a = -20 << 2
-20的二进制原码:10000000 00000000 00000000 00010100
-20的二进制反码:11111111 11111111 11111111 11101011
-20的二进制补码:11111111 11111111 11111111 11101100
左移两位后的补码:11111111 11111111 11111111 10110000
反码:11111111 11111111 11111111 10101111
原码:10000000 00000000 00000000 01010000
结果:a = -80
右移(>>)
高位补符号位,符号位不变,低位溢出。右移一位相当于除以2的一次方,右移n位相当于除以2的n次方。除不尽向下取整。
正数:a = 20 >> 2
20的二进制补码:00000000 00000000 00000000 00010100
向右移动两位后:00000000 00000000 00000000 00000101
结果:a = 5
负数:r = -20 >> 2
-20 的二进制原码 :10000000 00000000 00000000 00010100
-20 的二进制反码 :11111111 11111111 11111111 11101011
-20 的二进制补码 :11111111 11111111 11111111 11101100
右移两位后的补码:11111111 11111111 11111111 11111011
反码:11111111 11111111 11111111 11111010
原码:10000000 00000000 00000000 00000101
结果:r = -5
无符号右移(>>>)
高位补零,低位溢出。即若该数为正,则高位补0,而若该数为负数,则右移后高位同样补0
正数:a = 20 >>> 2 与 a = 20 >> 2相同
结果:a = 5
负数 a = -20 >>> 2
-20的二进制原码:10000000 00000000 00000000 00010100
-20的二进制反码:11111111 11111111 11111111 11101011
-20的二进制补码:11111111 11111111 11111111 11101100
右移两位后的补码:00111111 11111111 11111111 11111011 (正数的原码、反码、补码都是一样)
反码:00111111 11111111 11111111 11111011
原码:00111111 11111111 11111111 11111011
结果:a = 1073741819
位运算的使用奇巧淫技
判断奇偶数
int num = 5;
System.out.println(num + "是" + ((num&1) == 0 ? "偶数":"奇数"));
5的二进制代码为101,1的二进制代码为001,5 & 1 就是 101 & 001
我们通过二进制判断奇偶数的话,看的是二进制最后一位,如果最后一位为0的话是偶数,为1的话是奇数
为什么?因为二进制除了最后一位,其他位都是2的幂次方,必然是偶数,所以我们通过判断最后一位是0或者1 就可以判断是奇数还是偶数。
获取二进制位是1还是0
int n = 86;
System.out.println(n + "的第五位是" + (((n&(1<<4))>>4) == 0 ? "0":"1"));
System.out.println(n + "的第五位是" + (((n>>4)&1) == 0 ? "0":"1"));
交换两个整数变量的值
int num1 = 10;
int num2 = 20;
num1 = num1^num2;
num2 = num1^num2;
num1 = num1^num2;
System.out.println("num1=" + num1 + ", num2=" + num2);
异或运算满足的性质:
- 可交换性:ab=ba
- 可结合性:abc=(ab)c=a(bc)
- 自身进行异或运算值为零:a^a=0
- 与 0 异或时结果不变:a^0=a
num2可以看成是num1(num2num2),故结果为num1
num1可以看成是(num1num1)(num2num2)num2,故结果为num2
不用判断语句,求整数的绝对值
int a = -10;
int b = ((a >> 31) ^ a) + (a >>> 31);
System.out.println("a=" + a + ", b=" + b);
先来分析一下 a = -86:
原码:10000000 00000000 00000000 01010110
反码:11111111 11111111 11111111 10101001
补码:11111111 11111111 11111111 10101010
取反:00000000 00000000 00000000 01010101
(+1): 00000000 00000000 00000000 01010110
由此可以得出,负数的绝对值为,补码取反+1,即 ~a+1。
int a = -86;
int b = ~a + 1;
System.out.println(b);
但是,这个还不能达到题目所要求的,不用判断,当假定 a = -86的时候,就已经知到 a 是负数了。
分析:任何一个数 异或0 结果为其本身,异或-1 (二进制全为1) 相当于取反
而一个32 bit的int类型的正数 >> 31,结果为0,负数 >> 31 为-1。
即 (a >> 31) 的结果为 0 或者 -1。如果 a 为负数,(a >> 31) ^ a 相当于 ~a,则需要 + 1 才能得到 a 的绝对值
且一个32 bit的int类型的正数 >>> 31,结果为0,负数 >>> 31为1。
所以代码为:((a >> 31) ^ a) + (a >>> 31)
也可以写成:
int a = -86;
int b = a >> 31;
// 当a为正时:b = 0,a ^ b = a;
// 当a为负时:b = -1,a ^ b = ~a;
return (a ^ b) - b; // 也可以写为:(a ^ (a >> 31)) - (a >> 31)
常见面试题
Q:2*8怎么运算性能最好
A:我当时遇到这个面试题的时候是懵的,直接回答两个new BigDecimal();再进行运算就行了。*实际采用位运算的方式是性能最好的,因为计算机对于计算数据的话,都是以二进制来进行运算的,所以使用位运算相比直接使用(+、-、 、/)运算符,要更高效,能显著调高代码在计算机中的执行效率。相当于2 << 3。
Q:怎么交换两个整数变量的值
A:我当时遇到这个面试题的时候也是懵的,不知道这题的考点是啥,直接回答:创建一个临时变量,把其中一个变量赋值给临时变量,然后再把另一个变量赋值给这个变量,最后把临时变量赋值给另一个变量。实际直接采用异或运算即可,具体可见奇巧淫技中的交换两个整数变量的值。