在日常开发中位运算符可能使用的不多,但是在JDK或是Spring的源码中经常会发现这些优质开源的产品会经常使用。下面来简单回顾一下Java的运算符。

  • 算数运算符
+、-、 *、 /、 %
 
加、减、乘、除、取模
  • 关系运算符
<、>、<=、>=、==、!=

小于、 大于、 小于等于、 大于等于、 等于、 不等于
  • 逻辑运算符
&&、||、!、

逻辑与、逻辑或、取反、
  • 位运算符

  位运算起源于C语言的低级操作,Java的设计初衷是嵌入到电视机顶盒内,所以这种低级操作方式被保留下来。所谓的低级操作,是因为位运算的操作对象是二进制位,但是这种低级操作对计算机而言是非常简单直接,友好高效的。在简单的低成本处理器上,通常位运算比除法快得多,比乘法快几倍,有时比加法快得多。虽然由于较长的指令流水线和其他架构设计选择,现代处理器通常执行加法和乘法的速度与位运算一样快,但由于资源使用减少,位运算通常会使用较少的功率,所以在一些Java底层算法中,巧妙的使用位运算可以大量减少运行开销。

细化符号描述运算规则
按位运算&两位都为1,那么结果为1
有一位为1,那么结果为1
~~0 = 1,~1 = 0
^异或两位不相同,结果为1
移位运算<<左移各位二进制位全部左移N位,高位丢弃,低位补0
>>右移各位二进制全部右移N位,若值为正则高位插入0若值为负,则高位插入1
>>>无符号右移各位二进制全部右移N位,无论正负,都在高位插入0

  在进行位运算详解之前,先来普及下计算机中数字的表示方法。对于计算机而言,万物皆0、1,所有的数字最终都会转换成0、1的表示,有3种体现形式,分别是:原码、反码和补码。

  原码:原码表示法在数字前面增加了一位符号位,即最高位为符号位,正数位该位为0,负数位该位为1.比如十进制的5如果用8个二进制位来表示就是00000101,-5就是10000101。

  反码:正数的反码是其本身,负数的反码在其原码的基础上,符号位不变,其余各个位取反。5的反码就是00000101,而-5的则为11111010。

  补码:正数的补码是其本身,负数的补码在其原码的基础上,符号位不变,其余各位取反,最后+1。即在反码的基础上+1。5的反码就是00000101,而-5的则为11111011。

  了解了这几个概念后,我们现在先记住一个结论,那就是在计算机系统中,数字一律用补码来表示、运算和存储。

这篇文章进行了具体的说明

与运算(&

  • 规则:转为二进制后,两位为1,则结果为1,否则结果为0。
十进制二进制(正数原码、反码、补码一致)
1000000000000000000000000000001010
&12&00000000000000000000000000001100
==
800000000000000000000000000001000

或运算(|)

  • 规则:转为二进制后,有一位为1,则结果为1,否则结果为0。
十进制二进制(正数原码、反码、补码一致)
1000000000000000000000000000001010
|12|00000000000000000000000000001100
==
1400000000000000000000000000001110

非运算(~)

  • 规则:转为二进制后,~0 = 1,~1 = 0
十进制二进制(正数原码、反码、补码一致)
~7~00000000000000000000000000000111
==
-811111111111111111111111111111000

异或运算(^)

  • 规则:转为二进制后,两位不相同,结果为1,否则为0
十进制二进制(正数原码、反码、补码一致)
15^200000000000000000000000000001111^00000000000000000000000000000010
==
1300000000000000000000000000001101

左移运算(<<)

  • 规则:转为二进制后,各二进制位全部左移N位,高位丢弃,低位补0。
十进制二进制(正数原码、反码、补码一致)
2<<200000000000000000000000000000010
=0000000000000000000000000000001000
800000000000000000000000000001000

右移运算(>>)

  • 规则:转为二进制后,各二进制位全部右移N位,若值为正,则在高位插入 0,若值为负,则在高位插入 1
十进制二进制(正数原码、反码、补码一致)
2>>200000000000000000000000000000010
=0000000000000000000000000000000010
000000000000000000000000000000000

无符号右移运算(>>>)

  • 规则:转为二进制后,各二进制位全部右移N位,无论正负,都在高位插入0。
十进制二进制(先取补码 再对补码操作位移)
-1>>>110000000000000000000000000000001(原码)
11111111111111111111111111111110(反码)
11111111111111111111111111111111(补码)
011111111111111111111111111111111
01111111111111111111111111111111(补码)
01111111111111111111111111111110(反码)
溢出,只能表示到int的最大值214748364710000000000000000000000000000001(原码)

应用

不用额外的变量实现两个数字互换

    见参考资料中的BitOperationTest,方法reverse通过三次异或操作完成了两个变量值的替换。

    证明很简单,我们只需要明白异或运算满足下面规律(实际不止如下规律):

    0^a = a,a^a = 0;

    a ^ b = b ^ a;

    a ^ b ^ c = a ^ (b ^ c) = (a ^ b) ^ c;

    a ^ b ^ a = b;

    假设a,b两个变量,经过如下步骤完成值交换:a = a ^ b, b = b ^ a, a = a ^ b

    证明如下:

    因为a ^ b = b ^ a,又a = a ^ b, b = b ^ a。故b = b ^ a = b ^ ( a ^ b ) = a

    继续a = a ^ b,a =( a ^ b ) ^ b ^ ( a ^ b ),故a=b。完成值交换。

不用判断语句实现求绝对值

    公式如下:(a^(a>>31))-(a>>31)

    先整理一下使用位运算取绝对值的思路:若a为正数,则不变,需要用异或0保持的特点;若a为负数,则其补码为原码翻转每一位后+1,先求其原码,补码-1后再翻转每一位,此时需要使用异或1具有翻转的特点。

    任何正数右移31后只剩符号位0,最终结果为0,任何负数右移31后也只剩符号位1,溢出的31位截断,空出的31位补符号位1,最终结果为-1.右移31操作可以取得任何整数的符号位。

    那么综合上面的步骤,可得到公式。a>>31取得a的符号,若a为正数,a>>31等于0,a^0=a,不变;若a为负数,a>>31等于-1 ,a^-1翻转每一位。

判断一个数的奇偶性

    通过与运算判断奇偶数,伪代码如下:

    n & 1 == 1 ? ”奇数”:”偶数”

    奇数最低位肯定是1,而1的二进制最低位也是1,其他位都是0,所以所有奇数和1与运算结果肯定是1。

参考资料

ConcurrentProgramPractice wikipedia