在读jdk源码时,经常会遇到形如这样的代码:
public static int numberOfLeadingZeros(int i) {
// HD, Figure 5-6
if (i == 0)
return 32;
int n = 1;
if (i >>> 16 == 0) { n += 16; i <<= 16; }
if (i >>> 24 == 0) { n += 8; i <<= 8; }
if (i >>> 28 == 0) { n += 4; i <<= 4; }
if (i >>> 30 == 0) { n += 2; i <<= 2; }
n -= i >>> 31;
return n;
}
或者是这样:
if ((s & (s-1)) != 0)
由于上学时候对位操作认识不深,工作后运用也偏少,逐也渐渐淡忘,以致看到这般代码只能忽略而过,今天调试一个使用fork/join模型开发的工具时,再次遇到位运算,于是就上网找了很多资料,将位运算的基本原理和常见的应用场景整理下来。
一、关于二进制原码反码补码的基础知识
介绍位操作之前,先介绍原码、反码、补码相关知识。我们都知道,计算机底层运算都是以二进制的方式实现的,而我们平时生活计数都是用十进制,相应我们在使用高级语言编程中也都是使用十进制,那么我们理所当然认为,编译器把十进制转位二进制计算不就可以了吗,我们也这样假设。
我们都知道,java中int占4个字符,占4*8=32个字节,由于整数有正负则第一个字节存符号位,那么最大值就是2的31次方-1,那么二进制相加很简单:
1+1=2 二进制为: 0001(1)+0001(1)=0010(2) (为了方便阅读,我们只用4个字节,相当于byte)
那么问题来了,因为计算机只有加法操作,没有减法操作,所以减法操作是要转化为加法操作的,那么我们再看下面的代码:
1-1=1+(-1)=0 二进制:0001(1)-0001(1)=0001(1)+1001(-1)=1010(-2) 很显然是不对的。那么就需要制定一种规则,对需要参与运算的变量应用这种规则,可以实现对减法转为加法计算,结果为我们期望的值:假设规则为fit(),则需要满足如下条件:
fit(1)+fit(1)=fit(2)、fit(1)-fit(1)=fit(1)+fit(-1)=fit(0) 转为二进制则为:fit(0001)+fit(0001)=fit(0010)、fit(0001)-fit(0001)=fit(0001)+fit(1001)=fit(0000)
经过这帮人的琢磨,于是他们定了这么一个规则:【计算补码:负数的补码就是对反码加一,而正数不变,正数的原码反码补码是一样的。】
在补码中用(-128)代替了(-0),所以补码的表示范围为:(-128~0~127)共256个。
补码的计算很简单,在反码的基础上加一,比如1001求反码为1110(符号位不变,其他都取反),加一得1111。如此1-1的运算变为这样:
0001-10001=0001(正数补码与原码一样)+1111(负数补码)= 0000这就对了。
其实我们不难发现,负数的原码即为:正数的原码取反,这就是这就是补码的作用,这么一来,负数在计算机中的表示法就成了补码了。
二、位移运算
(1) << 左移
(2) >> 右移
(3) >>> 无符号右移
注意:位移操作只针对整型数据有效,包括int,byte,short,char,long,处int外,其他四种类型JVM会先把它们转换成int型再进行操作。
m<<n的含义:把整数m表示的二进制数左移n位,高位移出n位都舍弃,低位补0. (此时将会出现正数变成负数的形式)
实例(为了便于阅读,我们只用8个字节):
1)、 m<<n的含义:把整数m表示的二进制数左移n位,高位移出n位都舍弃,低位补0. (此时将会出现正数变成负数的形式)
3<<2剖析:
3 的二进制位00000011,左移两位后得到00001100,即为12. (12=3*2^2)
注意:左移可能使整数变为负数,即如果左移后最高位为1
2)、m>>n 的含义:把整数m表示的二进制数右移n位,m为正数,高位全部补0;m为负数,高位全部补1.
实例:
3>>2剖析:
3二进制形式: 00000011,右移2位得到00000000,即为0.
-3>>2剖析:
-3二进制形式: 11111101,右移2位,得到11111111,即为-1.(要注意了,这里看到的可是补码哈。)
以上:每个整数表示的二进制都是32位的,如果右移32位和右移0位的效果是一样的。依次类推,右移32的倍数位都一样。
3)、 m>>>n:整数m表示的二进制右移n位,不论正负数,高位都补零。
实例:
3>>>2剖析:
3二进制形式: 00000011,高位补零右移2位,得到00000000,即为0.
-3>>>2剖析:
-3二进制形式: 11111101,右移,得到00111111
【注】:对于以上三种操作,如果n为负数:这时JVM会先让n对32取模,变成一个绝对值小于32的负数,然后再加上32,直到 n 变成一个正数。
三、按位操作
1).~(按位非):对该整数的二进制形式逐位取反。
~4:(一元操作符)
4的二进制形式为:00000100,逐位取反后得到:11111011,即为-5.
2).|(按位或):对两个整数的二进制形式逐位进行逻辑或运算,原理为:1|0=1,0|0=0,1|1=1,0|1=1
等。
4|-5:
4的二进制形式为:00000100,
-5的二进制形式为:11111011,
逐位进行逻辑或运算:11111111,即得到-1.
3).&(按位与):对两个整数的二进制形式逐位进行逻辑与运算,原理:1|0=0,0|0=0,1&1=1;0&1=0等。
4&-5:
4的二进制形式为:00000100,
-5的二进制形式为:11111011,
逐位进行逻辑与运算:00000000,即得到0.
4).^(按位异或):【解义】对两个整数的二进制形式逐位进行逻辑异或运算,原理:1^1=0,1^0=1,0^1=1,0^0=0.
4^-5:
4的二进制形式为:00000100,
-5的二进制形式为:11111011,
逐位进行逻辑异或运算:11111111,即得到-1.
四、实际运用
1、m<<n即在数字没有溢出的前提下,对于正数和负数,左移n位都相当于m乘以2的n次方, m>>n即相当于m除以2的n次方,得到的为整数时,即为结果。如果结果为小数,此时会出现两种情况:(1)如果m为正数,得到的商会无条件的舍弃小数位;(2)如果m为负数,舍弃小数部分,然后把整数部分加+1得到位移后的值。 比如取半数就可以用n>>1
2、按位异或可以比较两个数字是否相等,它利用1^1=0,0^0=0的原理。 20^20==0
3、 判断int型变量a是奇数还是偶数
a&1 = 0 偶数
a&1 = 1 奇数
4、 求平均值,比如有两个int类型变量x、y,首先要求x+y的和,再除以2,但是有可能x+y的结果会超过int的最大表示范围,所以位运算就派上用场啦。
(x&y)+((x^y)>>1);
5、 对于一个大于0的整数,判断它是不是2的几次方
((x&(x-1))==0)&&(x!=0);
6、 比如有两个int类型变量x、y,要求两者数字交换,位运算的实现方法:性能绝对高效
x ^= y;
y ^= x;
x ^= y;
7、 取模运算,采用位运算实现:
a % (2^n) 等价于 a & (2^n - 1)
8、 a % 2 等价于 a & 1
public class NewPermission {
// 是否允许查询,二进制第1位,0表示否,1表示是
public static final int ALLOW_SELECT = 1 << 0; // 0001
// 是否允许新增,二进制第2位,0表示否,1表示是
public static final int ALLOW_INSERT = 1 << 1; // 0010
// 是否允许修改,二进制第3位,0表示否,1表示是
public static final int ALLOW_UPDATE = 1 << 2; // 0100
// 是否允许删除,二进制第4位,0表示否,1表示是
public static final int ALLOW_DELETE = 1 << 3; // 1000
// 存储目前的权限状态
private int flag;
/**
* 重新设置权限
*/
public void setPermission(int permission) {
flag = permission;
}
/**
* 添加一项或多项权限
*/
public void enable(int permission) {
flag |= permission;
}
/**
* 删除一项或多项权限
*/
public void disable(int permission) {
flag &= ~permission;
}
/**
* 是否拥某些权限
*/
public boolean isAllow(int permission) {
return (flag & permission) == permission;
}
/**
* 是否禁用了某些权限
*/
public boolean isNotAllow(int permission) {
return (flag & permission) == 0;
}
/**
* 是否仅仅拥有某些权限
*/
public boolean isOnlyAllow(int permission) {
return flag == permission;
}
}
详情看http://www.tuicool.com/articles/Zr6R3q
10、在图像处理方面使用比较多,比如处理颜色时,ARGB数据是一个int,
int color
可以用
((color & 0xFF000000)>>24) & 0xFF //得到A的值 ,也就是透明通道的值
((color & 0x00FF0000)>>16) & 0xFF //R 也就是红色的值
((color & 0x0000FF00)>>8 ) & 0xFF //G 绿色的值
((color & 0x000000FF) ) & 0xFF //B 蓝色的值
返过来也可以通过 A R G B的值来组成一个ARGB数据来进行处理。
当你要处理图颜色时 你就发现,位移非常方便。
11、大小写转换:小写转大写:(char)('a'&~32),大写转小写:(char)('A'|32),当然我们一般还是用jdk的原生库,这里只做介绍。