很多编程语言都有位运算符,Java语言也不例外。位运算符与我们之前学过的其他运算符不同,它是以一个二进制位上的值作为操作数的,也就是说:位运算符的操作数就是二进制位上的那个0或者1。Java语言提供了7种位运算符,如表8-1所示。
表8-1 Java语言位运算符
此处要特别提醒各位读者:位运算符是对long、int、short、byte和char这5种类型的数据进行运算的,不允许对double、float和boolean进行位运算操作。下面逐一讲解每种位运算符的运算规则。
1. 按位与运算符
按位与运算符的写法是一个“&”符号,与不短路的逻辑“与”运算符写法完全一样,但意义不同。逻辑“与”运算是对布尔型数据进行运算,而按位与运算符是对二进制位上的数值进行计算。按位与运算符的运算规则是:如果两个二进制位上的数都是1,那么运算结果为1,其他情况运算结果均为0,这个规则可以用下面的图8-11表示。
图8-11按位与运算符运算规则
如果对数字5和6进行按位与运算,其过程可以用下面的图8-12表示:
图8-12 5&6运算过程
运算过程中,首先把5和6这两个数字转换为补码,并把这两个数字按位对齐,然后一一把两个相应的二进制位上的数字进行按位与运算,运算得到的二进制数就是最终的结果。按照补码反向转换为十进制数字的规则,可以得出5&6的运算结果是4。此处还需说明:进行位运算的时候,最左边的符号位也要参与运算。
2. 按位或运算符
按位或运算符的写法是一个“|”符号,与不短路的逻辑“或”运算符写法相同。它的运算规则是:如果两个二进制位上的数字都为0,那么运算结果为0,否则运算结果是1。这个规则可以用图8-13表示:
图8-13 按位或运算符运算规则
如果对数字5和6进行按位或运算,其过程可以用下面的图8-14表示:
图8-12 5|6运算过程
首先还是把这两个数字转换成补码形式并对齐,之后把相应的二进制位上的数字进行按位或运算。经计算所得的二进制数111,将这个运算结果转换为十进制数是7。
3. 按位异或运算符
按位异或运算符写法是“^”,它的运算规则是:如果两个二进制位上的数字相同,则运算结果为0,如果两个二进制位上的数字不相同,则运算结果为1。这个规则可以用下面的图8-15表示:
图8-15 按位异或运算符运算规则
关于异或运算符,有以下特性:
(1)异或运算符满足交换律。
也就是说,a^b与b^a是等价的,虽然a和b交换了位置,但还是会运算出相同的结果。这个规律还可以推广到N个操作数,也就是说,如果有N个变量都参与了异或运算,那么它们的位置无论如何交换,运算的结果都是相同的。
(2)任何两个相同的数字进行异或操作,所得到的结果都必然为0。
这个特性并不难理解,因为两个相同的数字,换算成补码后,每个二进制位上的数也都相同,这样在进行异或运算时,按照运算规则,每个二进制位上得到的运算结果也都是0,这N个0所组成的二进制数就是0的补码。我们可以利用这个特性快速的判断两个整数是否相同。另外,利用这个特性还可以实现内存的快速清零操作,比如可以在代码中写上“a=a^a;”这条语句能快速的把变量a所占据的那几个字节的内存迅速清零。
(3)对于任意一个二进制位来说,这个位上的数与0进行异或运算,运算结果与这个二进制位上的数是相同的,而与1进行异或运算,结果与这个二进制位上的数字相反.
注意,此处所说的是二进制位上的数字,所谓相反不是说原来这个位上是1,运算结果是-1,而是说原来是1,运算结果为0,原来如果是0,运算结果是1,这才是这里所说的”相反”的概念。
(4)对于任何两个整数a和b,a^b^b等于a.
这个结论之所以成立,就是因为这个表达式中有b^b,而b^b的结果为0,前文已讲过,任何一个数与0进行按位异或操作,结果仍然是这个数本身,所以,a^b^b等于a。这个特性在加密运算方面有着很普遍的应用。我们可以把a当作要加密的数据,而把b当作密钥。a^b就是把a用密钥b进行了加密操作,当需要解密时,仍然以b作为密钥,再进行一次异或就实现了解密。
各位读者可以仿照按位与和按位或的计算过程推导一下5^6的运算结果。
4. 按位取反运算符
按位取反运算符写法是“~”,它的运算规则是:对每个二进制位进行取反操作,即原来二进制位上如果是0,那么就变成1,反之,如果原来二进制位上是1,那么就变为0。取反运算符是一个单目运算符,所以只需要一个操作数就可以了。假如对数字5进行按位取反操作,其运算过程如图8-16所示。
图8-16 ~5运算过程
从图8-16可以看出,这个运算结果是一个负数。按照负数补码规则,可以还原出这个运算结果是十进制数-6。此处需要提醒各位读者注意:对变量进行取反操作,变量的值并不会发生变化!为方便读者理解这句话的含义。
【例08_03 按位取反运算】
Exam08_03.java
- public class Exam08_03 {
- public static void main(String[] args) {
- int a = 5;
- System.out.println("对a按位取反的结果:"+~a);
- System.out.println("取反操作后a的值为:"+a);
- }
- }
【例08_03】的运行结果如图8-17所示。
图8-17 【例08_03】运行结果
从图8-17可以看出:取反操作后a的值还是5,这说明变量a经过取反得到的那个-6并没有被赋值到变量a中,由此可以证明:取反运算并没有对变量重新赋值的功能,因此变量本身的值不会因取反操作而发生改变。
除以上4种位运算符外,Java语言还提供了位移相关的位运算符。所谓”位移”就是指在内存中对二进制数进行移动的操作。与位移相关的运算符有三个,分别是: <<、>>、>>>,它们分别表示:左移、带符号右移和无符号右移
5 左移运算符
左移运算符的写法是“<<”,它表示要把二进制数据在内存空间中向左边移动。使用左移运算符时,把想进行位移操作的操作数放在左面,之后写上左移运算符,在左移运算符的右边写上移动的位数。例如:5<<2就表示对数字5进行左移2位的操作。下面的图8-18展示了数字5进行左移2位的操作后,二进制数在内存中是怎样变化的.
图8-18 5<<2运算示意
从图8-18可以看到:这个二进制数在内存中整体向左移动了两位,最左边的两位被移出内存单元,这两位数字将会被舍弃,右边空出的两位用0补齐。
左移运算有乘以2的N次方的效果。一个数向左移动1位,就相当于乘以2的1次方,移动两位就相当于乘以2的2次方,也就是乘以4。位移操作在实际运算时远远快于乘法操作,所以在某些对运算速度要求非常高的场合,可以考虑用左移代替乘以2的N次方的乘法操作。使用左移运算符时需要注意三个细节。
(1)位移操作同取反操作一样,并不能改变变量本身的值,例如:int型变量a的值是5,执行完a<<2的操作后,a的值仍然是5。
(2)当位移的位数很多时,导致最左边的符号位发生变化,就不再具有乘以2的N次方的效果了。比如十进制的5转换为补码形式是:前面29个0最后3位是101,如果移动29位,那么最前面的符号位就变成了1,此时运算的结果就成为了一个负数,不再是5乘以2的29次方的乘法结果。
(3)对于byte/short/int三种类型的数据,Java语言最多支持31位的位移运算。如果位移数超过31,则虚拟机会对位移数按连续减去32,直到得到一个小于32并且大于等于0的数,然后以这个数作为最终的位移数。例如对int型变量进行位移97位的操作,虚拟机会首先对97连续减去3个32,最终得到数字1,实际进行位移运算时会对变量只位移1位。而对于long类型的数据而言,最多支持63位的位移运算,如果位移数超过63,则连续减去64,以最终得到的小于64并且大于等于0的数作为位移数。
6. 带符号右移运算符
右移运算分为两种,分别是带符号右移和无符号右移。带符号右移运算符的写法是“>>”,与左移运算符的方向恰好相反。带符号右移就是指当二进制数向右边移动以后,左边空出的位用“符号位上的数字”填充,也就是说:如果是正数,二进制数右移的时候用0来填充左边的空位,而对于负数而言,右移的时候用1来填充左边的空位。图8-19和8-20分别展示了5和-5进行带符号右移2位后的结果:
图8-19 5>>2运算示意
图8-20 -5>>2运算示意
带符号右移具有“类似”除以2的N次方的效果。请注意,这里说的是“类似”除以2的N次方的效果,为什么要加上“类似”两个字呢?就是因为对于正数而言,带符号右移之后产生的数字确实等于除以2的N次方,比如说N的值为3,对于正15,带符号右移3位的结果是1,这个结果与“15除以2的3次方”的结果是相同的。但是对于负数而言,带符号右移的效果需要分为两种情况来讨论:
第一种情况:如果这个负数是“2的N次方”的整数倍,那么带符号右移N位的效果也等于除以2的N次方。例如:N的值为3,对于-16来说,它是“2的3次方”的整数倍,那么带符号右移3位的结果是-2,这个结果相当于“-16除以2的3次方”。
第二种情况:如果这个负数不是“2的N次方”的整数倍,那么带符号右移N位之后,是在除以2的N次方的结果之上还要减去1。例如:N的值还为3,对于-15来说,它不是“2的3次方”的整数倍,那么带符号右移3位的结果是-2,这个运算结果其实就是“-15除以2的3次方再减去1”。各位读者也可以用其他负整数来验证一下这个结论。
带符号右移的操作可以保证移动之前和移动之后数字的正负属性不变,原来是正数,不管移动多少位,移动之后还是正数,原来是负数,移动之后还是负数。另外,对于任何一个byte、short或者int类型的数据而言,带符号右移31位之后,得到的必然是0或者是-1。对于long类型的数据而言,带符号右移63位之后,得到的也必然是0或者是-1。能够得出这个结论的依据也很简单,就是因为对于byte、short和int类型的变量而言,如果是正数,带符号右移31位之后产生的二进制数的每一位必然全部是0,转换成对应的十进制数就是0;而对于负数而言,带符号右移31位之后产生的二进制数的每一位必然全部是1,转换成十进制数就是-1。对于long类型的数据,带符号右移63位也具有相同效果。
7. 无符号右移运算符
无符号右移运算符的写法是“>>>”,比带符号右移多了一个“>”。无符号右移操作在二进制数移动之后,空位由0来补充,与符号位是0还是1毫无关系,下面的图8-21和8-22分别展示了5和-5进行无符号右移2位后的结果:
图8-21 5>>>2运算示意
图8-22 -5>>>2运算示意
可以看出:对于正数而言,无符号右移和带符号右移没有什么区别,而对于负数而言,经过无符号右移会产生一个正数,因为最左边的符号位被0填充了。
以上讲解了7种位运算符的运算规则和注意事项,下面的【例08_04】演示了各种位运算符的用法和运算效果:
【例08_04 位运算符演示】
Exam08_04.java
- public class Exam08_04 {
- public static void main(String[] args) {
- int a = 5, b = -5, c = 6;
- long d = 10,e = -10;
- System.out.println("5&6=="+(a&c));
- System.out.println("5|6=="+(a|c));
- System.out.println("5^6=="+(a^c));
- System.out.println("~5=="+(~a));
- System.out.println("5<<2=="+(a<<2));
- System.out.println("-5<<2=="+(b<<2));
- System.out.println("5<<33=="+(a<<33));
- System.out.println("5>>2=="+(a>>2));
- System.out.println("-5>>2=="+(b>>2));
- System.out.println("5>>>2=="+(a>>>2));
- System.out.println("-5>>>2=="+(b>>>2));
- System.out.println("int型正数带符号右移31位=="+(a>>31));
- System.out.println("int型负数带符号右移31位=="+(b>>31));
- System.out.println("long型正数带符号右移63位=="+(d>>63));
- System.out.println("long型负数带符号右移63位=="+(e>>63));
- }
- }
建议各位读者先把例子中的每一个表达式都自己先手动计算一遍,然后再把计算结果与源程序的运行结果对照一下,这样能加深对各种位运算符的理解和记忆。