前言:程序中的所有数在计算机内存中都是以二进制的形式储存的。所以一些底层代码的操作也是有位运算的身影;对于机器而言,它们只认识0和1,因为这样更加容易理解和执行,速度也会更快。通过学习也有助于程序员理解底层操作,优化自己的代码,也可以从多个角度,多个思路来解决问题。
位运算: 用于对整数类型变量的二进制形式进行操作。
- 位逻辑运算符:
& (位 “与”) and
^ (位 “异或”)
| (位 “或”) or
~ (位 “取反”) - 移位运算符:
(左移) <<
(右移) >>
(>>>)表示不带符号向右移动二进制数,移动后前面统统补0;两个箭头表示带符号移动。
没有<<<这种运算符,因为左移都是补零,没有正负数的区别。
优先级
位“与”、位“或”和位“异或”运算符都是双目运算符,其结合性都是从左向右的,优先级高于逻辑运算符,低于比较运算符,且从高到低依次为&、^、|
在Java中Integer包装类型有将十进制转化为二进制的方法 toBinaryString()
int a = 33, b = 36;
System.out.println("默认十进制输出 a : " + a);
System.out.println("默认十进制输出 b : " + b);
System.out.println("二进制输出");
System.out.println("转换二进制输出 a : " + Integer.toBinaryString(a));
System.out.println("转换二进制输出 b : " + Integer.toBinaryString(b));
输出结果为
可以看到33的二进制为100001,而36的二进制为100100。
十进制转为二进制就是除以2能不能除的掉,能就是0,不行就是1。
像100100,我们通常将前面的1叫做高位,而最后面的0就叫做低位(底位)。
1. 按位与 “&”
int n1 = a & b;
System.out.println("a & b : "+n1);
System.out.println("a & b : "+Integer.toBinaryString(n1));
System.out.println(" & 与运算 两个位都为 1 时,结果才为 1");
System.out.println("【口诀】: 与同 1 为 1");
我们将33与36,会发现他的十进制结果是32,二进制的结果为100000
&的口诀:与同1为1
说明:为什么a&b等于32,利用与(&)的口诀,与同1为1。a的二进制是100001,b的二进制是100100,两者的比较图如下:
可以看到,从右边开始是2的0次方,往左依次增加,有1的地方就累加下来,就是他的十进制结果。而a&b的结果就是32了。
2. 按位或 “|”
int n2 = a | b;
System.out.println("a | b : "+n2);
System.out.println("a | b : "+Integer.toBinaryString(n2));
System.out.println(" | 或运算 两个位都为 0 时,结果才为 0");
System.out.println("【口诀】: 或同 0 为 0");
那么如果我们将33或36呢?
或的口诀:或同0为0
二进制的特点:
除了底位是1,其他位都是2的次方,也就是说,除了底位是奇数,其他位都是偶数。所以如果是奇偶数的判断,只需要判断底位上放的是0还是1。
那么我们可以来一个判断奇偶数判断的技巧
// 与运算的技巧使用 【判断奇偶数】
if((a & 1) == 1){
System.out.println("奇数 : " + a);
}else{
System.out.println("偶数 : " + a);
System.out.println("奇数 & 1 : " + (a & 1));// 注意优先级
System.out.println("偶数 & 1 : " + (b & 1));
System.out.println("【口诀】: 1 前补 0 高归底 奇偶只看底 10");
}
只要它的底位是1的话,那么他就是奇数了,否则就是偶数。
也可以这样子写:
System.out.println("((奇数 >> 1 << 1) == 自己) : " + ((a >> 1 << 1) == a)); // 注意优先级
System.out.println("((偶数 >> 1 << 1) == 自己) : " + ((b >> 1 << 1) == b));
这里的<< 和 >> 在下文会介绍到
3. 按位异或 “^”
int n3 = a ^ b;
System.out.println("a ^ b : "+n3);
System.out.println("a ^ b : "+Integer.toBinaryString(n3));
System.out.println(" ^ 异或 两个位相同为 0,相异为 1");
System.out.println("【口诀】: 异或 同为 0 异为 1");
异或的口诀:异或 同为 0 异为 1
// 异或运算的技巧使用 【自己和 0】
int n4 = a ^ a;
System.out.println("a ^ a : "+n4);
int n5 = a ^ 0;
System.out.println("a ^ 0 : "+n5);
System.out.println("【口诀】: 异或自己为 0 异或 0 为自己");
这里还有一个小技巧,如果异或的是自己的话,则为0,异或0的话则为自己。
口诀:异或自己为 0 异或 0 为自己
我们平常有遇到两个变量交换值的操作,一般我们都是会用一个临时的变量temp变量来就行交换。而如果高级一点的玩法的话就会用到异或。
// 异或运算的技巧使用 【交换两个数】
a = a ^ b; // 符合结合律
b = a ^ b;
a = a ^ b;
System.out.println("默认十进制输出 a : "+a);
System.out.println("默认十进制输出 b : "+b);
那如果想要更加老练的话呢,就利用到了交换律了。
a ^= b; // 符合交换律
b ^= a;
a ^= b;
System.out.println("默认十进制输出 a : "+a);
System.out.println("默认十进制输出 b : "+b);
System.out.println("【口诀】: 我异或等你 你异或等我 我异或等你 我们就不是自己");
当然,这些交换只是整形类型的交换,如果是数组或者对象的交换,还是要用一个temp变量的。
4. 按位非 “~”
int n6 = ~a;
System.out.println("~a : "+n6);
// ~x = -(x+1) ~33 = -34
int n7 = ~n6;
// ~x = -(x+1) ~(-34) = -(-34+1) = 33
System.out.println("~~a : "+n7);
System.out.println(" ~ 取反 0 变 1,1 变 0");
System.out.println("【口诀】: 加 1 取负就是位取反 但他不是逻辑取反 反反还是自己哦");
(非)~ 的其实就是取反,但它不是逻辑取反。
(非~)的口诀: 加 1 取负就是位取反 但他不是逻辑取反 反反还是自己哦
说到这里,前面的口诀实属有点多,而且容易混乱,特地全部总结了一下,以防忘记。
同1则为1(&),同0则为0(|)
异或 同为 0,异或 异为1
异或自己为0,异或0为自己
非常手段要加1,括号记得别忘记
5. 左移运算符 “<<”
int n8 = a << 2;
System.out.println("a << 2 : "+n8);
System.out.println("a << 2 : "+Integer.toBinaryString(n8));
int n9 = a << 34;
System.out.println("a << 34 : "+n9);
System.out.println("a << 34 : "+Integer.toBinaryString(n9));
System.out.println(" << 左移 各二进位全部左移若干位,高位丢弃,低位补 0");
System.out.println("【口诀】: 左移 N 就是乘以 2 的 N 次方 超出则模 2 的 32 次方");
左移其实就是转化为二进制之后全部往左边移动,也可以认为就是乘以2的 N 次方(N代表移动的位数),最大是2的32次方。
像代码中 a << 2就是左移2位,换成十进制就是乘以2的2次方,所以33乘以4等于132。如果是移动34的话,就相当于走了大半天绕回了原点,还是2次方。
例如 a * 32 == a << 5
如果 a * 31 == a << 5 - a 因为他等于32的次方减去一个a
(左移<<)的口诀: 左移 N 就是乘以 2 的 N 次方 超出则模 2 的 32 次方
6. 右移运算符 “>>”
int n10 = a >> 2;
System.out.println("a >> 2 : "+n10);
System.out.println("a >> 2 : "+Integer.toBinaryString(n10));
int n11 = a >> 34;
System.out.println("a >> 34 : "+n11);
System.out.println("a >> 34 : "+Integer.toBinaryString(n11));
System.out.println(" >> 右移 各二进位全部右移若干位,对无符号数,高位补 0,有符号数,各编译器处理方法不一样,有的补符号位(算术右移),有的补 0(逻辑右移)");
System.out.println("【口诀】: 右移 N 就是除以 2 的 N 次方 超出则 N-32");
右移其实和左移差不多,只不过相反而已,就是往右边移动了,而是除以2的N次方。33除以2的2次方,四舍五入得出结果是8。
(右移>>)的口诀:右移 N 就是除以 2 的 N 次方 超出则 N-32
求a绝对值
通常我们会用到下面这种写法
if(a>=0) {
return a;
}else {
return -a;
}
但是如果用位运算的角度来看的话,就可以这么写
// 求 a 的相反数 -a 除了 (-1*a) 也可以 (~a+1)
// 求 a 的 绝对值 |a| a>>31==0?a:(~a+1)
a>>31==0?a:(~a+1)
这里相当于右移31位,只保留最高位的数,如果是0,那么就是a自己本身了,如果是负数,那我们就取反,变成他的绝对值。
7. 右移运算符 “>>>”
(>>>)表示无符号右移,也叫逻辑右移,即若该数为正,则高位补0,而若该数为负数,则右移后高位同样补0
正数: r = 20 >>> 2 的结果与 r = 20 >> 2 相同;
负数: r = -20 >>> 2 的结果是1073741819
注:以下数据类型默认为int 32位
如果想深入了解一下原码、反码、补码可以看看这篇文章 Java 原码、反码、补码分析。