说明:本文介绍的位运算符不包含移位运算符,移位运算符在另一篇文章
目录
-
Java位运算符
-
位与运算符 &
- 运算规则:真真为真,其余都是假。 相应的每一位都是1则为1,否则为0。
对于二进制位: 1 & 1 = 1 1 & 0 = 0 0 & 0 = 0
-
实例:
7 和 12 的二进制只有在第三位都是1,所以得出结果为4。 -
总结:位与运算可以获取两个数对应位相同部分,任意数字与0位与都为0,与-1位与结果为本身。
- 运算规则:真真为真,其余都是假。 相应的每一位都是1则为1,否则为0。
-
位或运算符 |
- 运算规则:假假为假,其余都是真。相应的每一位都是0则为0,否则为1。
对于二进制位: 1 | 1 = 1 1 | 0 = 1 0 | 0 = 0
- 实例:
7 和 12 做位或运算,只要两个数的位中存在1,都会得到1,所以最终结果是后4位都是1,得到15。 - 总结:位或运算可以保留两个二进制的所有1。负数的负号也会保留下来,因为位运算在计算时,符号位也会参与计算,所以如果与负数做位或操作,那么结果为负数。任意数与0位或,结果为本身;与 -1 位或,结果为 -1。
- 运算规则:假假为假,其余都是真。相应的每一位都是0则为0,否则为1。
-
位非运算符 ~
- 运算规则:对二进制位的每一位都取反,即 1 取反为 0,0 取反为 1。
- 实例:
由上图可知:反转0的每一位, 取反结果为 -1。 - 总结:取反运算将每一位都反转,正数取反得到负数,负数取反得到正数。用取反运算可以计算出相反数,取反后 +1即为相反数,论证过程在下述实例求相反数中。
-
位异或运算符 ^
- 运算规则:真真为假,假假为真,真假为真。相应的每一位不同为1,相同则为0。
1 ^ 1 = 0 0 ^ 0 = 0 1 ^ 0 = 1
- 实例:
还是 7 和 12,计算示意图如下,不同的位将得到1,所以结果为11。 - 总结:位异或可以保留两个数的不同部分且满足以下3条:
- 交换律,a ^ b = b ^ a,多个变量亦如此,a ^ b ^ c = b ^ c ^ a。
- 任何两个相同数字进行异或,结果为0。
- 对于一个二进制来说,这个位上的数与另一个二进制位上的0异或,运算结果是这个位上的数不变;如果是与二进制位上的1异或,那么结果这个位上的数为相反数,即1 -> 0, 0 -> 1。 所以一个数异或0,结果为本身,一个数异或 -1,结果为相反数。
- 运算规则:真真为假,假假为真,真假为真。相应的每一位不同为1,相同则为0。
-
-
位运算符使用实例分析
-
判断奇偶数
- 代码:
static void judgeEvenOrOdd(int num) { int result = num & 1; if (result == 0) { System.out.println(num + "是偶数"); } else { System.out.println(num + "是奇数"); } }
- 分析:
由上图可以看出,由于1的二进制形式,只有最后1位为1,所以其他的数字与1进行位与运算,只需要看最后1位结果。奇数的定义是,不能被2整除的数,偶数是能被2整除。所以对于奇数的二进制,最后1位必定是1,对于偶数的二进制,最后1位必定是0。所以当位与结果为0时,可判断出该数字是偶数,当位与结果为1时,可判断出该数字是奇数。
- 代码:
-
取余运算
- 代码:
static void surplus(int num) { // 对2的幂取余,num & (2的幂 -1),假设对 4 取余 int remainder = num & (4 - 1); System.out.println(remainder); }
- 分析:
余数的定义是整除后被除数未除尽的部分,且余数的取值范围 [0, 除数 - 1]。
2的幂在二进制中有一个特点,只存在一个1的位,其余都是0。
以2的幂的二进制的角度来看,余数的范围是 [0, 2的幂 - 1],二进制表现余数最大值为:2的幂的二进制中的高位1置为0,高位右侧全置为1。
所以计算2的幂的余数也就是求出数字在高位1右侧的值。
由上图二进制形式可以看出:4的二进制最后3位是100,3的二进制最后3位是 011,余数的取值范围为:[0, 3],符合上面的推论。
取余运算可以根据这个特性,只要与3进行位与运算,由于位与运算可以保留出相同部分,那么可以获取数字的后两位二进制,即为对4的取余结果。
- 代码:
-
判断一个数是不是2的幂
- 代码:
static void isPowerOf2(int num) { int result = num & (num - 1); String msg = result == 0 ? "num是2的幂": "num不是2的幂"; System.out.println(msg); }
- 分析: 判断一个数是否是2的幂,如果是2的幂,应满足:正整数,二进制中只存在一个1。所以若是2的幂,当num - 1后,原最高位变为0,右侧都是1,与原二进制进行位与运算应该等于0。可参照取余运算中4和3的二进制示意图。
- 代码:
-
判断数字的正负号
- 代码:
static void judgeTheSignOfNumber(int num) { int judge = num >> 31; String str = judge == 0 ? "正数" : "负数"; System.out.println(str); }
- 分析:判断正负的原理是,右移31位将符号位移动到最后1位,左侧会填充符号位的值,如果是正数,右移31位,应该等于0,负数右移后等于-1,0与正数结果一致,不考虑。用无符号右移也可以,只是负数无符号右移后变成1而不是-1,论述过程
- 代码:
-
hashmap的tableSizeFor方法
- 代码:
static int MAXIMUM_CAPACITY = 1 << 30; // 求出大于等于 cap 的第一个2的幂 static int tableSizeFor(int cap) { int n = cap - 1; 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; }
- 分析:这是hashmap中的原方法,目的是求出大于等于给定值的第一个2的幂。那么对于一个2的幂来说,一定是个正整数,且二进制形式中只包含一个1。举个例子,给定一个数 292,求比它大的第一个2的幂。根据292的二进制形式可知,第一个比它大的2的幂一定是低9位为0,第10位为1,也就是 10 0000 0000 ,明确了需要获取什么样的二进制形式,下面的过程就很容易理解了。
首先分析中间的位运算部分:
1. 无符号右移1位,将n的最高位移至最高位右1位,此时进行或运算,可得原最高位和右1位都是1,也就是将高位1右侧1位也变成了1,现在是两个1。
2. 无符号右移2位,将上一步获得的n的最高位2位(因为上面的或运算,所以都是1),再次移动2位,进行或运算,可获得最高位右4位都是1。
3. 无符号右移4位,与上面同理,可获得最高位右8位都是1。由于例子中使用的数字原因,这一步及后续的位运算的操作对这个数已经没有影响了。
4. 无符号右移8位,可得最高位右16位都是1。
5. 无符号右移16位,可得最高位右32位都是1,由于int类型长度为32位,除去符号位是31位,所以到这一步已经能将所有的位都变为1了。
位运算后的最终结果是511,二进制形式中,后9位都是1。在开始已经分析过,最终需要的是一个 10 0000 0000这样的二进制形式,那么现在已经将后9位都置为了1,剩下的只需要+1进位就可以得到目标二进制,所以最后是 n + 1。
再来分析为什么在方法的开头要先减1。先举个例子,如果传入的参数是一个2的幂:
那么经过位运算后,得到的结果是15,再 + 1 也就变成了 16,虽然是大于8的2的幂,但是我们希望输入如果是2的幂,返回的是输入参数。毕竟如果输入7,那么返回8,输入8也应该返回8而不是16,如果作为容量,则会有一半空间浪费。所以为了满足这种情况,主动将参数减1,这样获得的结果将是最合适的。
方法外定义的 MAXIMUM_CAPACITY,是int类型中2的幂的最大值。
- 代码:
-
求相反数
- 代码:
static void getOppositeNum(int num) { int result = ~ num + 1; System.out.println(result); }
- 分析:相反数的定义:绝对值相等,正负号相反的两个数互为相反数。
作为相反数,区别只是在正负符号不同而已,在二进制形式上,只是符号位不同。
对于正数来说,其实是求出原码除符号位外与正数相同的负数。由于计算机内部都是使用补码进行运算,
正数的补码和原码一样,负数则不同。负数的补码计算过程为:除符号位外都求反,然后 +1。
而取反运算是将所有位都求反,可以分成两步:第一步将符号位求反,变成与正数互为相反数的负数的原码;
第二步按照计算负数计算补码过程,将除符号位外其他位求反,此时只需要 +1 就是负数的补码了,所以对一个正数取反再 +1即可得到其相反数。
对于负数来说,其实是求出与其原码除符号位外都相同的正数,负数的反码取反运算可以获得负数的相反数。补码 -1 再求反可以获得相反数,
先对补码求反再 +1也可以获得相反数。
- 代码:
-
求绝对值
- 代码:
static void getAbsoluteValue(int num) { int judge = num >> 31; // 第一种方式 int result = judge == 0 ? num : (~ num + 1); System.out.println(result); // 第二种方式 int result2 = (num ^ judge) - judge; System.out.println(result2); // 第三种方式 int result3 = (num + judge) ^ judge; System.out.println(result3); }
- 分析:
获取绝对值,首先判断正负,judge只有两个可能,正数为0,负数为-1。
第一种方式:判断judge的值,正数直接取本身,负数的绝对值等于负数的相反数。
第二种方式:任何数与0异或都不变,与 -1 异或相当于取反。所以可以使得异或后的结果再减去judge,如果num是负数,则judge为 -1,也就相当于 +1,所以就变成了 ~ num +1,正数judge为0,结果就等于num。
第三种方式:若judge为0,num为正数,表达式变为 (num + 0) ^ 0 = num;若judge为 -1,num为负数,表达式为 (num - 1) ^ (-1) = ~ (num - 1),由于负数的补码是在反码基础上 +1,所以 num - 1 = num 的反码,再次求反,获得num的绝对值(符号位已经反转)。
- 代码:
-
交换两个变量(不引入第三个变量)
- 代码:
static void exchangeVariables(int x, int y) { System.out.println(String.format("交换前,x = %s, y = %s", x, y)); x ^= y; y ^= x; x ^= y; System.out.println(String.format("交换后,x = %s, y = %s", x, y)); }
- 分析:由于一个数异或另一个数两次等于异或0,等于本身,所以:
x ^= y ==> x = x ^ y
y ^= x ==> y = y ^ x ==> y = y ^ x ^ y ==> y = x ^ 0 = x
x ^= y ==> x = x ^ y ==> x = x ^ y ^ x = y ^ 0 = y
主要是进行变量替换,即可推导出结果。
- 代码:
-
判断两个数正负号是否相同
- 代码:
static void isSameSymbol(int a, int b) { boolean same; if (a == b) { same = true; } else { same = (a ^ b) > 0; } System.out.println(same); }
- 分析:判断两个数字是否符号相同,即同正或同负。
只要判断符号位即可,由于异或相同为0,所以若符号位相同,即同符号,那么异或结果的符号位为0,结果为正数;
若符号位不同,那么异或结果的符号位为1,结果为负数。
- 代码:
-
求两个数的平均数
- 代码:
static void getAverage(int x, int y) { int average = (x & y) + ((x ^ y) >> 1); System.out.println(average); }
- 分析:求平均值:直接使用 (x + y) / 2 或 (x + y) >> 1,在 x + y 结果超过int能表示的最大值时不可用。
计算平均值时,可以直接将两数相加除以2,也可以拆分,将两个数字拆为相同部分和不同部分。
举个例子,比如 14 和 12 相加求平均值,按十进制,可以拆分为 10 + 4,10 + 2,那么两数相同部分为10,(10 + 10) / 2 = 10,
所以可以任意取一个10作为相同部分求出的平均值。不同部分求平均值 (4 + 2) / 2 = 3,所以最终的平均值也就是 10 + 3 = 13。
按照这个思想,将x与y分成两部分来计算,第一部分为相同位部分:获取相同位,x & y,那么平均数也就等于其中任意一个;
第二部分计算不同部分:获取不同部分之和,x ^ y,由于是二进制,所以不同位一定是不能进位的情况,也就是一个是1,一个是0,
对于这样的部分相加结果也就是1,所以加法也可以用异或运算来代替,x + y = x ^ y。最后除以2求平均值,即为 (x + y) >> 1。
将两部分加起来就是两个数的平均值。
- 代码:
-
求两个数的最大值
- 代码:
static void getMaximum(int x, int y) { int maximum = y & ((x - y) >> 31) | x & (~ (x - y) >> 31); System.out.println(maximum); }
- 分析:如果 x 大于 y,那么就应该保留x,舍弃y,(x - y) >> 31 结果是0,~ (x - y) >> 31 结果为 -1。
y & ((x - y) >> 31) -> y & 0 = 0,所以左半边为0。
x & (~ (x - y) >> 31) -> x & (-1) = x,所以 0 | x = x,最大值为x。
如果 x 小于 y,同理应该保留y,舍弃x:y & ((x - y) >> 31) -> y & (-1) = y,
x & (~ (x - y) >> 31) -> x & 0 = 0,所以 y | 0 = y,最大值为y。
如果 x 等于 y,那么保留任意一个即可: y & ((x - y) >> 31) -> y & 0 = 0,
x & (~ (x - y) >> 31) -> x & (-1) = x,所以最大值为x。
- 代码:
-
求两个数的最小值
- 代码:
static void getMinimum(int x, int y) { int minimum = x & ((x - y) >> 31) | y & (~ (x - y) >> 31); System.out.println(minimum); }
- 分析:与求最大值思想类似,消除最大值,保留最小值。
获取最小值。如果 x 大于 y:x & ((x - y) >> 31) -> x & 0 = 0,
y & (~ (x - y) >> 31) -> y & (-1) = y, 所以 0 | y = y,最小值为y。
如果 x 小于 y:x & ((x - y) >> 31) -> x & (-1) = x,
y & (~ (x - y) >> 31) -> y & 0 = 0, x | 0 = x,所以最小值为x。
如果 x 等于 y:x & ((x - y) >> 31) -> x & 0 = 0,
y & (~ (x - y) >> 31) -> y & (-1) = y, 0 | y = y,所以最小值为y。
- 代码:
-