Java位运算和几个使用场景解析

说明:本文介绍的位运算符不包含移位运算符,移位运算符在另一篇文章

目录

Java位运算符

位与运算符 &

位或运算符 | 

位非运算符 ~

位异或运算符 ^ 

位运算符使用实例分析

判断奇偶数

取余运算

判断一个数是不是2的幂

判断数字的正负号

hashmap的tableSizeFor方法

求相反数

求绝对值

交换两个变量(不引入第三个变量)

判断两个数正负号是否相同

求两个数的平均数

求两个数的最大值

求两个数的最小值


  • Java位运算符

    • 位与运算符 &

      • 运算规则:真真为真,其余都是假。 相应的每一位都是1则为1,否则为0。
        对于二进制位:
        1 & 1 = 1
        1 & 0 = 0
        0 & 0 = 0

         

      • 实例:
        7 和 12 的二进制只有在第三位都是1,所以得出结果为4。

      • 总结:位与运算可以获取两个数对应位相同部分,任意数字与0位与都为0,与-1位与结果为本身。

    • 位或运算符 | 

      • 运算规则:假假为假,其余都是真。相应的每一位都是0则为0,否则为1。
        对于二进制位:
        1 | 1 = 1
        1 | 0 = 1
        0 | 0 = 0

         

      • 实例:
        7 和 12 做位或运算,只要两个数的位中存在1,都会得到1,所以最终结果是后4位都是1,得到15。
      • 总结:位或运算可以保留两个二进制的所有1。负数的负号也会保留下来,因为位运算在计算时,符号位也会参与计算,所以如果与负数做位或操作,那么结果为负数。任意数与0位或,结果为本身;与 -1 位或,结果为 -1。
    • 位非运算符 ~

      • 运算规则:对二进制位的每一位都取反,即 1 取反为 0,0 取反为 1。
      • 实例:


        由上图可知:反转0的每一位, 取反结果为 -1。
      • 总结:取反运算将每一位都反转,正数取反得到负数,负数取反得到正数。用取反运算可以计算出相反数,取反后 +1即为相反数,论证过程在下述实例求相反数中。
    • 位异或运算符 ^ 

      • 运算规则:真真为假,假假为真,真假为真。相应的每一位不同为1,相同则为0。
        1 ^ 1 = 0
        0 ^ 0 = 0
        1 ^ 0 = 1

         

      • 实例:
        还是 7 和 12,计算示意图如下,不同的位将得到1,所以结果为11。
      • 总结:位异或可以保留两个数的不同部分且满足以下3条:
        1. 交换律,a ^ b = b ^ a,多个变量亦如此,a ^ b ^ c = b ^ c ^ a。
        2. 任何两个相同数字进行异或,结果为0。
        3. 对于一个二进制来说,这个位上的数与另一个二进制位上的0异或,运算结果是这个位上的数不变;如果是与二进制位上的1异或,那么结果这个位上的数为相反数,即1 -> 0, 0 -> 1。 所以一个数异或0,结果为本身,一个数异或 -1,结果为相反数。
  • 位运算符使用实例分析

    • 判断奇偶数

      • 代码:
        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。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值