位运算符主要是针对整型数据类型的二进制值进行操作的运算符,因为它是直接操作的二进制值所以他的执行效率非常高,远超于普通的加减乘除运算,但是缺点也很明显:比较难理解,可读性也很差。所以一般在开发中我们很少会用到。
处于性能考虑在一些较为底层的类库中一般使用频率较高,比如在jdk的源码中就大量的使用,下面就简单介绍一下位运算符,不用泰国纠结怎么使用,至少我们什么时候碰到了这样的代码之后不会一脸懵逼就行了。
下面是位运算符的简介
操作符 | 介绍 |
---|---|
& | 按位“与”,如果相对应位都是1,则该位为1,否则为0 |
| | 按位“或”,如果相对应位都是0,则该位为0,否则为1 |
~ | 按位“非”,按位取反,翻转操作数的每一位,即01互换 |
^ | 按位“亦或”,如果相对应位值相同,则该位为0,否则为1 |
<< | 左移操作符,左操作数按位左移右操作数指定的位数 |
>> | 右移操作符,左操作数按位右移右操作数指定的位数 |
>>> | 无符号右移,左操作数的值按右操作数指定的位数右移,移动得到的空位以零填充 |
OK,talk is cheap, show me the code. 只是说还是比较抽象的,下面用代码来演示一下
首先为了展示更形象一些我们先声明一个小工具方法获取一个整型数字的更为工整的二进制值
这个方法有两个参数,第一个是需要转换的具体数字,第二个是需要得到的位数,我们可先先使用这个方法,等最后理解了这个位操作符之后可以回头再来看这个方法
public static void main(String[] args) {
int left = 117;
int right = 15;
System.err.println("参数1: " + toBinary(left, 8));
System.err.println("参数2: " + toBinary(right, 8));
System.err.println("& 操作后: " + toBinary(left & right, 8));
System.err.println("| 操作后: " + toBinary(left | right, 8));
System.err.println("~ 操作后: " + toBinary(~left, 8));
System.err.println("^ 操作后: " + toBinary(left ^ right, 8));
System.err.println("<< 操作后: " + toBinary(left << 2, 8));
System.err.println(">> 整数操作后:" + toBinary(left >> 2, 8));
System.err.println("负数值: " + toBinary(-left, 8));
System.err.println(">> 负数操作后:" + toBinary(-left >> 2, 8));
System.err.println(">>> 操作后: " + toBinary(left >>> 2, 8));
System.err.println("负数>>>操作后:" + toBinary(-left >>> 2, 8));
// output:
参数1: 01110101
参数2: 00001111
& 操作后: 00000101
| 操作后: 01111111
~ 操作后: 1111111111111111111111110001010
^ 操作后: 01111010
<< 操作后: 11010100
>> 整数操作后:00011101
负数值: 1111111111111111111111110001011
>> 负数操作后:1111111111111111111111111100010
>>> 操作后: 00011101
负数>>>操作后:11111111111111111111111100010
}
public static String toBinary(int num, int digit) {
int value = 1 << digit | num;
String bs = Integer.toBinaryString(value);
return bs.substring(1);
}
& 按位“与”
按位与操作符,两边操作数字的二进制值都为1的位数则为1,否者为0
一位一位分析:
第一行: 0 1 1 1 0 1 0 1
x x x x x √ x √
第二行: 0 0 0 0 1 1 1 1
结 果: 0 0 0 0 0 1 0 1
可以看到,把两个二进制值逐位对比,按照&操作符的规则,x表示不符合规则,||表示符合规则,最下方就是最后的出的结果,一目了然
| 按位“或”
按位或操作符,两边操作数字的二进制值只要有一位为1则为1,否者为0
第一行: 0 1 1 1 0 1 0 1
x √ √ √ √ √ √ √
第二行: 0 0 0 0 1 1 1 1
结 果: 0 1 1 1 1 1 1 1
~ 按位“非”
按位“非”,这是一个单目运算符,将操作的每一位都取反,即0变1,1变0,也就是取一个数字的二进制反码
原 码: 0 1 1 1 0 1 0 1
结 果: 1 0 0 0 1 0 1 0
上方| 操作输出的位数是32位,因为int的最大值就是32位表示的,如果我们取一个数字的多位二级制值,为空的则用零表示,例如3=0003,这里则就把数字最大位数之前的所有零也进行了反转,结果就如上边所示占满了int值的最大位数,如果再把它转换位10进制这个值就是-118,刚好是117的原码的补码,补码就是负数的表现形式
^ 按位“亦非”
两个相同下标位置的二进制数字相同则为0,否则为1
第一行: 0 1 1 1 0 1 0 1
x √ √ √ √ √ √ √
第二行: 0 0 0 0 1 1 1 1
结 果: 0 1 1 1 1 0 1 0
<< 左移操作符
将操作数的二进制码整体左移操作符右边的指定位数,右边空出的以0填充
原 码: 0 0 0 1 1 1 0 1 0 1
结 果: 0 1 1 1 0 1 0 1 0 0
\ /
新增
为了对比这里在原码的前边多加了两位0,可以看到整体左移之后结果的右边以0填充,可以很清楚的看出对应关系
>> 右移操作符
跟左移操作符类似,只不过是把原码向右移动,但是因为负数的二进制码是和整数的二进制码的反码再补码,这样如果直接右移就会有前边符号填充的情况不同,如果是整数则以0填充,如果是负数则以1填充
下面是取出有用位数的结果:
正数值右移2位
原 码: 0 1 1 1 0 1 0 1
结 果: 0 0 0 1 1 1 0 1 [0 1]
\ / \/
新增 舍去
负数值右移2位
原 码: 1 0 0 0 1 0 1 1
结 果: 1 1 1 0 0 0 1 0 1 1
\ / \/
新增 舍去
>>> 无符号右移
将操作数的二进制码整体右移指定位数,左边空出来的位不再根据整数还是负数填充,全部以0填充
如果直接使用一个正数来进行右移的话那就和>>操作等价,所以下边展示的是负数的右移
但是观察上方的代码会发现,负数变成了正数,并且非常的大。这是因为整型数字的正负号是以第一位的数字判断的,0表示正数,1则表示负数,但是因为无符号右移都是以0填充昨天空出来的位数,导致本来表示正负数的第一位数字也就是1变成了0,那这个数字就变成了数字,并且由于后边的在负数中原来表示为0的1还是1,变成正数以后就会出现非常大的数字这种情况
原 码: 1111111111111111111111110001011
结 果: 11111111111111111111111100010
仔细看负数的原码和进行无符号右移后的原码,右移后的原码比原来的原码少了两位,那是因为前两位都以0进行填充,而展示的时候0自动被省略了,所以原码应该是这样
原 码: 1111111111111111111111110001011
结 果: 0011111111111111111111111100010
现在我们可以回过头来再看那个获取指定二进制位数的工具方法了
int value = 1 << digits | num;
这一句就是核心,我们一步一步进行剖析
首先是1 << digits, 1的二进制码也还是1, 但是左边却有很多的零,如下
0000000000000000000000000000001
把这一个二进制码进行左移指定的位数,假定为8,那1的右边就被填充8个0,结果如下
100000000
然后再用这个二进制码和传进来的需要获取二进制码的参数进行按位或操作,也就是把那个参数的二进制码
和100000000进行按位或操作,如下, 加入获取数字1的八位二进制码
int num = 1;//00001
1 << 8 | 000000001;
100000000 | 000000001
100000001// 257
00000001//1
结果就是257,然后最后再使用substring()方法把第一个1给截取到,结果就是00000001字符串