Java中的位运算
引子
在上一篇文章《计算机信息的表示与存储》中,我详细地总结了原码、补码等表示方式以及其计算。而这些的目的,就是为了引出本文——位运算相关的知识。这里,我将仔细讲一讲位运算相关的知识。
位运算基础
在Java中,位运算操作分为两种:按位操作 和 移位操作。和C/C++相同,Java中也支持 按位非(~)、按位与(&)、按位或( | ) 和 按位异或(^) 四个按位操作,以及 左移(<<) 和 有符号右移(>>) 两个移位操作。此外,Java还额外支持了一种移位操作:无符号右移:>>> 。这是C/C++中所没有的操作。下面我会详细介绍这些操作。
按位操作符
首先看一看 按位操作符 和 逻辑运算操作符的对比:
! :非 | ~ :按位非,也叫做“取反” |
&& :与,且 | & :按位与 |
|| :或 | | :按位或 |
^ :按位异或 |
按位操作符用来操作整数基本数据类型中的一个二进制位(即1个bit)。
在下面的例子中,我都使用1个字节来存放整数。
按位非:~
按位非 ~ 是一个单目运算符,其作用是是二进制数的每一位取反。
例:求 ~25 = ?
首先,分析题目,要求 ~25的值。在计算机中,数值都是以补码的形式存储的。25是个正数,则其原码、反码和补码都一样,很简单就能计算得到 25 的补码为 00011001。然后我们再对其取反:
对其取反后,得到的结果记为R,则R仍然是补码。很明显R为负数,我们再对其求补码,即可得到R对应的原码:
经过计算得出,R对应的原码为 10011010,即 R = -26。所以,~25 = -26。
按位与:&
按位与 & 操作的作用是将两个操作数的每一位分别进行 逻辑与(&&) 操作。即对应两个位都是 1 ,则得到 1,否则得到 0 。
例:求 25 & -16 = ?
首先,我们分别求得 25 的补码为 00011001,16的补码为 11110000。
计算的结果即为R,则R的补码为 0001000。显然R为正数,则 R 的原码也为 00010000,即 R = 16。所以 25&-16 = 16。
按位或:|
按位或 | 操作的作用是将两个操作数的每一位分别进行 逻辑或( || ) 操作。即对应两个位只要有一位是 1 ,则得到 1,否则得到 0 。
例:求 25 | 35 = ?
首先,很容易我们可以得到25的补码为 00011001,35的补码为 00100011,然后计算:
结果记为R,则 R 的补码为 00111011,显然是正数,则R的原码也为 00111011,即 R = 59。所以 25|35 = 59。
按位异或:^
按位异或 ^ 操作的作用是将两个操作数的每一位分别进行 异或操作,操作逻辑是:如果两个对应位相同,则返回 0;若对应位不同,则返回 1。
例:求 0x39 ^ 0x2a = ?
首先,我们将两个16进制的数转换为二进制的数,并求出它们对应的补码:
然后,进行计算:
记过记为R,则R的补码为 00010011,显然R是个正数。则 R的原码也为 00010011,即 R = 19。所以 0x39 ^ 0x2a = 19。
移位操作符
上面介绍了四种按位操作符,下面,我再介绍三种Java中支持的移位操作符。
移位操作符操作的对象也是数据的二进制位。位移操作只能用来处理整数类型。
如果对 char、byte、或者 short 类型的数值进行移位处理,在移位处理前,它们会被转换为 int 类型(32位),并且得到的也是一个 int 类型的值。并且,只有 移动位数的 低五位 起作用,因为 2^5 = 32,而 int 型值就是32位的。也就是说,移动距离在 0~31 之间。
例如:如果有 25 >> 35。35的补码为:
仅低五位起作用,也就是说,实际上执行的是 25 >> 3 = 3。
如果是处理一个 long 型的值,那最后得到的结果也是 long 型。同理,此时只有移动位数的 低六位 起作用。
在下面的例子中,范例中的数字,都使用1个字节存储。所有数字都是 byte 类型。
左移:<<
左移操作符 << 可以将 操作符左边的操作数 向左移动 操作符右侧指定的位数,并在右侧 补 0 。
例:求 25 << 2 = ?
首先,我们计算出 25 的补码为 00011001,然后进行移位:
上图中,中间那还一部分0,我简化了一下图的画法。
结果记为R,显然R为正数。R补码的低八位为 01100100,因为高位都为0,为了简化计算,我们直接看 低8位。则R低八位的原码也为 01100100,即R = 100。所以 25 << 2 = 100。
带符号右移:>>
带符号右移操作符 >> 可以将 操作符左边的操作数 向右移动 操作符右侧指定的位数。如果符号位为 正,则在高位插入 0;如果符号位为 负,则在高位插入 1 。
例一:求 25 >> 2 = ?
我们很容易得到 25 的补码,低八位为 00011001,进行移位:
结果记为R,显然为正数,R低八位的补码为 00000110。则R低八位的原码也为 00000110,即R = 6。所以 25 >> 2 = 6。
例二:求 -25 >> -30 = ?
很容易求得 -25 的补码。这里需要注意的是,右移的位数为 -30,是个负数。我们求 -30 的补码,低八位如下图:
-25在移位时会被转化为 int 类型,所以移动位数只有低五位才会生效。记生效位数的值为 T,则 T 的原码为 00000010,T = 2。
也就是说, -25 >> -30 实际上等价于 -25 >> 2。然后对其进行移位操作:
结果记为R,显然R为负数。我们对其求补码,得到R对应的原码为
R的低八位为 00000111,符号位为 1,即R = -7。所以 -25 >> 2 = -7。
Ps:
实际上,因为 -25在移位时会自动转换为 int类型。int 类型 32 位,所以,当移动位数为 负数 时,移动结果相当于移动了(移动位数 + 32)。
同理,如果 需要进行移位操作的操作数 是 long 类型的,当移动位数为 负数 时,移动结果相当于移动了(移动位数 + 64)。例如:
289L >> -60 == 289L >> (64-60) == 289L >> 4 == 18。
无符号右移:>>>
无符号右移 >>> 移动方式与 带符号右移 >> 一样,不同的是,无符号右移采用零扩展:无论正负,都在高位插入 0 。这个操作是 C/C++中没有的。
例:求 -25 >>> 2 = ?
直接看移动过程:
结果记为R,显然R为正数。则R的补码和原码相同,都是如图所示,即 R = 1073741817。所以, -25 >>> 2 = 1073741817。
总结
位运算是非常强大的,在项目中合理地运用位运算,可以使代码更有逼格。最主要的原因是,看JDK或一些框架的源码的时候,经常会碰到位运算,不了解位运算,就不能很好滴学习大佬们的代码。总结完位运算,感觉神清气爽啊~
参考文档
1、《C++语言程序设计》第四版,郑莉·著。第二章:C++简单程序设计
2、《Java编程思想》第四版,Bruce Eckel·著。第三章:操作符