1 概述
在Java语言中,提供了7种位运算符,分别是按位与(&)、按位或(|)、按位异或(^)、取反(~)、左移(<<)、带符号右移(>>)和无符号右移(>>>)。
这些运算符当中,仅有~是单目运算符,其他运算符均为双目运算符。
- 对数值类型数据进行按位操作;1表示true、0表示false。
- 按位运算表示按每个二进制位(bit)进行计算,其操作数和运算结果都是整型值。
按位运算表示按每个二进制位(bit)进行计算,其操作数和运算结果都是整型值。
位运算符是对long、int、short、byte和char这5种类型的数据进行运算的,我们不能对double、float和boolean进行位运算操作。
位操作是程序设计中对位模式按位或二进制数的一元和二元操作。
在许多古老的微处理器上, 位运算比加减运算略快, 通常位运算比乘除法运算要快很多。
在现代架构中, 情况并非如此:位运算的运算速度通常与加法运算相同(仍然快于乘法运算)。
凡是位运算符,都是把值先转换成二进制,再进行后续的处理(因为位运算是针对二进制数进行的一种运算,对于十进制这些数值的位运算来说,会先将其转为二进制,再对其进行位运算,之后将运算结果再转为十进制。)
操作符 | 语义 | 描述 | |
& | 按位与 | 二进制数据按位与操作 | 两个位都为1时,结果才为1 |
| | 按位或 | 二进制数据按位或操作 | 两个位都为0时,结果才为0 |
~ | 按位取反 | 二进制数据按位取反 | 0变1,1变0 |
^ | 按位异或 | 二进制数据异或,相同为假、不同为真 | 两个位相同为0,相异为1 |
>> | 右移 | 二进制数据整体右移 | 各二进位全部右移若干位,对无符号数,高位补0;有符号数,各编译器处理方法不一样,有的补符号位(算数右移),有的补0(逻辑右移) |
<< | 左移 | 二进制数据整体左移 | 各二进位全部左移若干位,高位丢弃,低位补0 |
>>> | 无符号右移 | 二进制数据整体右移,符号位补0 | (相当于除2)左边补0 |
字符串连接符:+
如果字符串和基本数据类型变量、常量连接以后,结果变成了字符串。
示例:
public class Program { public static void main(String[] args) { int n1 = 6; //0000 0000 0000 0000 0000 0000 0000 0110 int n2 = 11; //0000 0000 0000 0000 0000 0000 0000 1011 System.out.println(n1&n2); //2(0010)二进制数据按位与操作 System.out.println(n1|n2); //15 (1111)二进制数据按位或操作 System.out.println(~n1); //-7 即转换为1111111111111111111111111111111111111111111111111111111111111001按位取反(补码存储) System.out.println(n1^n2); //13 (1101)按位异或,相同为假、不同为真 //text02 int n11 = 5; //0000 0000 0000 0000 0000 0000 0000 0101 int n22 = 7; //0000 0000 0000 0000 0000 0000 0000 0111 System.out.println(n11&n22); //5 (101)二进制数据按位与操作 System.out.println(n11|n22); //7 (111)二进制数据按位或操作 System.out.println(~n11); //-6 即转换为1111····1111 1010按位取反(补码存储) System.out.println(n11^n22); //2 (010--2)按位异或,相同为假、不同为真 //右移>>(相当于除2)左边补符号位 int n3 = 20; //二进制(1 0100) int n4=n3>>1; //10(01010) (最右右移1,最左补符号位,正数补0负数补1) int n5=n3>>2; //5(101) int n6=n3>>5; //0(移完了) int n7=n3>>32; //32的倍数,不变 System.out.println("n4是"+n4); System.out.println("n5是"+n5); System.out.println("n6是"+n6); System.out.println("n7是"+n7); System.out.println("---------------"); //左移<<(相当于乘2)右边补0 //无符号右移>>>(相当于除2)左边补0 int n8 = 30; n8 = n8>>>1; //15 int n9 = -30; n9 = n9>>>1; //2147483633 System.out.println("n8是"+n8); System.out.println("n9是"+n9); } }
右移>>(相当于除2)左边补符号位
左移<<(相当于乘2)右边补0
无符号右移>>>(相当于除2)左边补0
public class Program01 { //1.有一个整数8747,获取这个整数的二进制后四位 //2.有一个整数8747,获取这个整数的二进制的第二个后四位 public static void main(String[] args) { int x = 8747; //0010 0010 0010 1011 System.out.println(x&0xF); //11 System.out.println(x>>>4&0xF);//2 } }
2 七种位运算符的运算规则
2.1 &按位与运算符
&按位与,是将两边的数先转换为二进制位,然后运算最终值
运算规则 :如果两个二进制位上的数都是1,那么运算结果为1,其他情况运算结果均为0。
(0&0=0 1&1=1 1&0=0 0&1=0)
public class Demo { public static void main(String[] args) { // &按位与,运算符两边的条件值会先转换为2进制,然后再比对运算 /** * 0 0 1 1 * 0 1 0 1 * ----------- * 0 0 0 1 */ int a = 3&5; /** * 0 0 0 1 * 0 0 1 0 * ----------- * 0 0 0 0 */ int b = 1&2; System.out.println(a);// 1 System.out.println(b);// 0 } }
2.2 |按位或运算符
| 按位或和&按位与计算方式都是:先转换为二进制数,再计算
运算规则:两个二进制位上的数字如果都为0,那么运算结果为0,否则运算结果是1。
(0|0=0 0|1=1 1|0=1 1|1=1)
public class Demo { public static void main(String[] args) { // |按位或,运算符两边的条件值会先转换为2进制,然后再比对运算 /** * 0 0 1 1 * 0 1 0 1 * ------------- * 0 1 1 1 */ int a = 3|5; /** * 0 0 0 1 * 0 0 1 0 * ------------- * 0 0 1 1 */ int b = 1|2; System.out.println(a);// 7 System.out.println(b);// 3 } }
2.3 ~取反运算符
按位取反运算符写法是”~”,它的运算规则是:
对每个二进制位进行取反操作,所谓取反就是原来二进制位上如果是0,那么就变成1,反之,如果原来二进制位上是1,那么就变为0。即:取反就是1为0,0为1。
如果原来二进制位上是1,那么就变为0。即:取反就是1为0,0为1。
取反运算符是一个单目运算符,所以只需要一个操作数就可以了。
例如:5的二进制位是0000 0101,取反后为1111 1010,值为-6
public class Demo { public static void main(String[] args) { /** * 0 0 0 0 0 1 0 1 * --------------------- * 1 1 1 1 1 0 1 0 * * 1、如果 11111010 想转为负的十进制,因为最高位是1,所以先将11111001减1得到11111001 * 2、取反 00010101,然后计算出 00000100 对应的十进制为6 * 3、所以 11111010 最终对应的十进制为 -6 * * 1 1 1 1 1 0 1 0 * - 0 0 0 0 0 0 0 1 * --------------------- * 1 1 1 1 1 0 0 1 * --------------------- * 0 0 0 0 0 1 0 0 */ System.out.println(~5); // -6 } }
2.4 ^异或运算符
^异或运算符顾名思义,异就是不同
运算规则:两个二进制位上的数字如果相同,则运算结果为0;
如果两个二进制位上的数字不相同,则运算结果为1。
(1^0 = 1 , 1^1 = 0 , 0^1 = 1 , 0^0 = 0)
//比如说:ab与ba是等价的,虽然a和b交换了位置,但还是会运算出相同的结果。
注意:
- 如果该数与0进行异或运算,运算结果与这个数是相同的;
- 如果该数与1进行异或运算,则结果与这个数字相反;
- 当然,所谓相反不是说原来这个位上是1,运算结果是-1,而是说原来是1,运算结果为0,原来如果是0,运算结果是1,这才是此处所说的”相反”的概念。
public class Demo { public static void main(String[] args) { /** * 0 0 1 1 * 0 1 0 1 * ------------- * 0 1 1 0 */ int a = 3^5; /** * 0 0 0 1 * 0 0 1 0 * ------------- * 0 0 1 1 */ int b = 1^2; System.out.println(a);// 6 System.out.println(b);// 3 } }
//异或法—(面试题)不使用第三个变量交换两个数字,
x=x^y;
y=x^y;
x=x^y;
//面试题:不使用第三个变量交换两个数字 public class Demo { public static void main(String[] args) { int x =10;int y = 20; //加减法;缺点,如果数据大可能会出现溢出 x=x+y; y=x-y; x=x-y; //异或法 x=x^y; y=x^y; x=x^y; System.out.println("交换后"+x+" "+y); } }
2.5 <<左移运算符
5<<2的意思为5的二进制位往左挪两位,右边补05的二进制位是0000 0101 , 就是把有效值101往左挪两位就是0001 0100 ,正数左边第一位补0,负数补1,等于乘于2的n次方,十进制位是5*2^2=20
#乘以2的N次方:
左移运算有乘以2的N次方的效果。一个数向左移动1位,就相当于乘以2的1次方,移动两位就相当于乘以2的2次方,也就是乘以4。
位移操作在实际运算时远远快于乘法操作,所以在某些对运算速度要求非常高的场合,可以考虑用左移代替乘以2的N次方的乘法操作。
public class Demo { public static void main(String[] args) { /** * 5<<2的意思为5的二进制位往左挪两位,右边补0 * 0 0 0 0 0 1 0 1 * 0 0 0 1 0 1 0 0 */ System.out.println(5<<2); // 20 } }
注意三个细节:
- 首先:位移操作同取反操作一样,并不能改变变量本身的值,所能改变的仅是存储在操作数栈中那个数据的值
public class Demo { public static void main(String[] args) { int a = 5; System.out.println(a<<2); //20 左移2位结果为20 System.out.println(a); //5 但a的值没有改变 } }
其次:当位移的位数很多时,导致最左边的符号位发生变化,就不再具有乘以2的N次方的效果了。比如十进制的5转换为补码形式是:前面29个0最后3位是101,如果移动29位,那么最前面的符号位就变成了1,
- 此时运算的结果就成为了一个负数,不再是5乘以2的29次方的乘法结果。
- 其次:当位移的位数很多时,导致最左边的符号位发生变化,就不再具有乘以2的N次方的效果了。比如十进制的5转换为补码形式是:前面29个0最后3位是101,如果移动29位,那么最前面的符号位就变成了1,此时运算的结果就成为了一个负数,不再是5乘以2的29次方的乘法结果。
如果位移数超过31,则虚拟机会对位移数按连续减去32,直到得到一个小于32并且大于等于0的数,然后以这个数作为最终的位移数。
例如:对int型变量进行位移97位的操作,虚拟机会首先对97连续减去3个32,最终得到数字1,实际进行位移运算时只对变量位移1位。
而对于long类型的数据而言,最多支持63位的位移运算,如果位移数超过63,则连续减去64,以最终得到的小于64并且大于等于0的数作为位移数。
数字5左移32位的倍数还是数字本身。
2.6 >>右移运算符> (不要余数)
右移运算分为两种,分别是带符号右移和无符号右移。首先我们来说说带符号右移运算符。带符号右移运算符的写法是”>>“,与左移运算符的方向恰好相反。
所谓带符号右移就是指当二进制串向右边移动以后,左边空出的位用”符号位上的数字”填充,
说的更直白一点,如果是正数,二进制串右移的时候用0来填充左边的空位,而对于负数而言,右移的时候用1来填充左边的空位,如下:
eg: 5>>2
右移前001000000000000000000000000000000101
右移后00001000000000000000000000000000000101
eg: -5>>2
右移前11111111111111111111111111111011
右移后1111111111111111111111111111111011
左移N位的操作具有乘以2的N次方的效果,其实带符号右移也具有”类似”除以2的N次方的效果。
例如:5的二进制位是0000 0101,右移两位就是把101左移后为0000 0001,正数左边第一位补0,负数补1,等于除于2的n次方,结果为1
public class Demo { public static void main(String[] args) { /** * 5>>2的意思为5的二进制位往右挪两位,左边补0 * 0 0 0 0 0 1 0 1 * 0 0 0 0 0 0 0 1 */ System.out.println(5<<2); // 1 } }
类似”除以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。比如,对于-15来说,它不是“2的3次方”的整数倍,那么带符号右移3位的结果是-2,这个运算结果其实就是“-15被2的3次方整除再减去1”。小伙伴们也可以用其他负整数来验证一下这个结论。因为并非每个负整数带符号右移的结果都等于除以“2的N次方”,所以我们才在文中添加了“类似”这两个字。
带符号右移的操作可以保证移动之前和移动之后数字的正负属性不变,原来是正数,不管移动多少位,移动之后还是正数,原来是负数,移动之后还是负数。
小结论:
另外,我们还可以继续深挖一下这个特性,从而得到一个结论:对于任何一个byte、short或者int类型的数据而言,带符号右移31位之后,得到的必然是0或者是-1。对于long类型的数据而言,带符号右移63位之后,得到的也必然是0或者是-1。
能够得出这个结论的依据也很简单,就是因为对于byte、short和int类型的变量而言,如果是正数,带符号右移31位之后产生的二进制串必然全部是0,转换成对应的十进制数就是0;而对于负数而言,带符号右移31位之后产生的二进制串必然全部是1,转换成十进制数就是-1。对于long类型的数据,带符号右移63位也具有相同效果。
2.7 >>>无符号右移运算符
无符号右移的运算规则与有符号右移的运算规则差别就在于:无符号右移在二进制串移动之后,空位由0来补充,与符号位是0还是1毫无关系。
对于正数而言,无符号右移和带符号右移没有什么区别,而对于负数而言,经过无符号右移会产生一个正数,因为最左边的符号位被0填充了。
- 正数无符号右移
无符号右移运算符和右移运算符的主要区别在于负数的计算,因为无符号右移是高位补0,移多少位补多少个0。
//15的二进制位是0000 1111 , 右移2位0000 0011,结果为3 public class Demo { public static void main(String[] args) { /** * 0000 1111 * --------------- * 0000 0011 */ System.out.println(15>>>2); // 3 } }
负数无符号右移
-6的二进制是6的二进制取反再加1,
6的二进制也就是0000 0000 0000 0000 0000 0000 0000 0110,
取反后加1为1111 1111 1111 1111 1111 1111 1111 1010,
右移三位0001 1111 1111 1111 1111 1111 1111 1111
public class Demo { public static void main(String[] args) { System.out.println(-6>>>3); // 536870911 } }
3 常见用法
3.1 位运算实现乘除法:
通常如果需要乘以或除以2的n次方,都可以用移位的方法代替
3.2 位运算交换两整数
void Swap(int &a, int &b) { if (a != b) { int c = a; a = b; b = c; } }
如果要求不 使用第三个变量来实现,则需要使用位运算
void Swap(int &a, int &b) { if (a != b) { a ^= b; b ^= a; a ^= b; } } /** * 面试题交换数字 * 编写一个函数,不用临时变量,直接交换numbers=[a,b]中a与b的值。 * 示例 * 输入:numbers = [1,2] * 输出:[2,1] * 提示: * numbers.length ==2 * -2147483647<=numbers[i]<=2147483647 * 第一步 a^=b 即a=(a^b); * 第二步 第二步 b^=a 即b=b(ab),由于运算满足交换律,b(ab)=bb^a。 * 由于一个数和自己异或的结果为0并且任何数与0异或都会不变的,所以此时b被赋上了a的值。 * 第三步 a^=b 就是a=ab,由于前面二步可知a=(ab),b=a,所以a=ab即a=(ab)^a。故a会被赋上b的值。 */
3.3 位运算判断奇偶数
通过二进制判断奇偶:该数二进制的最后一位是0的话那么就为偶数;是1的话就为奇数
#include<iostream> using namespace std; int main() { int i; for(i=0;i<100;++i) { if(1==(1&i)) cout<<i<<"是奇数"<<endl; else cout<<i<<"是偶数"<<endl; } return 0; }
3.4 位运算改变正负性
正数变成负数,负数变成正数
/** * 将-11变成11 * 1111 0101(二进制) –取反-> 0000 1010(二进制) –加1-> 0000 1011(二进制) * 将11变成-11 * 0000 1011(二进制) –取反-> 0000 0100(二进制) –加1-> 1111 0101(二进制) * 因此变换符号只需要取反后加1即可 */ int SignReversal(int a) { return ~a + 1; }
3.5 求绝对值
对应的复数改变为正数,就是取反+1,正数不变
int my_abs(int a) { int i = a >> 29; return i == 0 ? a : (~a + 1); }
对于任何数,与0异或都会保持不变,与-1即0xFFFFFFFF异或就相当于取反。因此,a与i异或后再减i(因为i为0或-1,所以减i即是要么加0要么加1)也可以得到绝对值。
int my_abs(int a) { int i = a >> 29; return ((a ^ i) - i); }
4 十进制与二进制转换练习
- 十转二十进制数10转为二进制
10除以2 得5 余数为 0 再拿刚才的5除以2得2 余数为1 2除以2得1 余数为0 1除以2 直接余数为1 那么就为**1010**
10 = 8 + 2 = 2^3 + 2^1 2^3 转化为二进制可以看做是1后面跟着后面3个零即 1000 2^1 转为为二进制可以看做是1后面跟着后面1个零即 10 1000 + 10 ———————— 1010 位数对齐后相加,满二进位
2.二转十
二进制数1110转为十进制
1.
1011
这是个二进制数,所以每个数都有2的影子。所以我们从右边数,
1转为 1*2^0等于11转为 1*2^1等于2
0转为 0*2^2等于0
1转为1*2^3等于8
1+2+0+8=11
2.
1110
=1000 + 100 + 10
= 10^3 + 10^2 + 10^1
————————
然后二进制转为十进制,需要把底数10转为2得到
= 2^3 + 2^2 + 2^1
=14