1.原码、反码、补码
关于原码、反码、补码的相关知识作者不打算在这里长篇大论,相关知识已有别的大佬总结很好了,还请老铁自行 Google,不过有篇知乎回答是作者学编程以来见过对相关知识最通俗易懂,生动简洁的解释:对原码、反码、补码最通俗易懂,生动简洁的解释,墙裂建议大家先看完这篇科普文章。在继续讨论之前你要先明白一点:整数在计算机内部都是以补码形式存储的。
2.Java 位运算概览
OK 都看到这儿了那我就假定你已经掌握了原码、反码、补码相关知识(虽然上面那段几乎啥也没讲,纯凑字数)
不废话了。
首先,在程序中,位运算符的优先级很低!且位运算是只针对整型(是的这里指所有整型)数据的,无论正负,先搞清楚这两点,我们继续说。
Java 中的位运算共有 与(&)、或(|)、非(~)、异或(^)、左移(<<)、右移(>>)、无符号右移(>>>) 这7种,概览见下表:
符号 | 含义 | 详解 |
---|---|---|
& | 位与 | 两个比特位都为 1 时,结果才为 1,否则为 0 (位与操作满足交换律和结合律,甚至分配律) |
| | 位或 | 两个比特位都为 0 时,结果才为 0,否则为 1 (位或操作满足交换律和结合律,甚至分配律) |
~ | 位非 | 即按位取反,1 变 0,0 变 1 |
^ | 异或 | 两个比特位相同时(都为 0 或都为 1)为 0,相异为 1(异或操作满足交换律和结合律,甚至分配律。任何整数和自己异或的结果为 0,任何整数与 0 异或值不变) |
<< | 左移 | 将所有的二进制位按值向←左移动若干位,左移操作与正负数无关,它只是傻傻地将所有位按值向左移动,高位舍弃,低位补 0 |
>> | 右移 | 将所有的二进制位按值向右→移动若干位,低位直接舍弃,跟正负无关,而高位补 0 还是补 1 则跟被操作整数的正负相关,正数补 0 ,负数补 1 |
>>> | 无符号右移 | 将所有的二进制位按值向右→移动若干位,低位直接舍弃,跟正负无关,高位补 0 ,也跟正负无关 |
这里有几点需要注意:
- 这七个位运算符,只有取反(~)运算符是单目运算符,其他均为双目运算符!
- 注意上面提到的某几个位运算操作满足交换律和结合律甚至分配律,这很重要!后面你会看到。
- 额好吧我目前只想到上面两点需要注意的,别的想到我会更新文章!
还要说一下在 Kotlin 里面,位运算跟 Java 一致也是这七种,只不过运算符号跟 Java 里有点不太一样,Kotlin 里的七种位运算符:
- and(bits) – 位与 (Java 的 &)
- or(bits) – 位或 (Java 的 |)
- inv() – 位非 (Java 的 ~)
- xor(bits) – 位异或 (Java 的 ^)
- shl(bits) – 有符号左移 (Java 的 <<)
- shr(bits) – 有符号右移 (Java 的 >>)
- ushr(bits) – 无符号右移 (Java 的 >>>)
嗯除了写法其他跟 Java 中一致!
下面我们来看些 Java 中位运算的代码实例。
3.Java 位运算最佳实践(常见操作)
1.位运算基础操作
//对于任意整数 a 和 b,以下表达式的结果均为 true,各位读者朋友可自行验证,真不骗你!
//注意以下几个操作之所以一一在这里特别列出,是因为它们对于位操作而言具有某种普遍意义,也就是说你最好能将它们牢记于心。
//至于它们有怎样的普遍意义,留给读者们自行思考,总之记住实践出真知!
a|0 == a;
a&1 == a;
a&0 == 0;
a^a == 0;
a^0 == a;
a|~a == -1;
a&~a == 0;
a&a == a;
a|a == a;
a|(a&b) == a;
a&(a|b) == a;
2.位运算进阶操作(部分操作参考了别的大佬博客):
1.判断奇偶,
可以很简单归纳出,只需判断一个正整数的二进制补码最后一位是0是1,是0为偶数,是1为奇数
public void isOddOrEven(int n){
if ((n & 1) == 1) {//n是奇数
System.out.println("Odd");
}else {//n是偶数
System.out.println("Even");
}
}
2.省去中间变量交换两整数的值(据说面试常考)
public void swap(){
int a = 1, b = 2;
a ^= b;
b ^= a;//b == 1
a ^= b;//a == 2
System.out.println("a:" + a);//a:2
System.out.println("b:" + b);//b:1
}
3.变换符号,正变负,负变正
只需对待操作数应用取反操作后再加 1 即可
public void negate(){
int a = -10, b = 10;
System.out.println(~a + 1);//10
System.out.println(~b + 1);//-10
}
4.求绝对值,
对于负数可以通过上面变换符号的操作得到绝对值,
正数直接返回即可,因此我们要先判断符号位来得知当前数的正负。
public int abs(int a){
int i = a >> 31;//得到符号位,0 为正数,-1 为负数
return i == 0 ? a : (~a + 1);//符号位为 0 直接返回,否则返回 ~a + 1
}
或者,n>>31 取得n的符号,若n为正数,n>>31等于0,若n为负数,n>>31等于-1
若n为正数 n^0-0 数不变,若n为负数n^-1 需要计算n和-1的补码,异或后再取补码,
结果n变号并且绝对值减1,再减去-1就是绝对值
public int abs(int a){
return a ^ (a >> 31)) - (a >> 31);
}
5. 判断一个数num是不是 2 的幂
如果是2的幂,n一定是100… (也就是二进制位里只有一个是1,且是首位,其余全是0),n-1就是011…. 所以做与运算结果为 0,这里贴下 Kotlin 代码:
fun isPowerOfTwo(num: Int): Boolean {
if (num < 1){
return false
}
return num.and(num - 1) == 0
}
6.判断一个数num是不是 4 的幂
理论上数字4幂的二进制类似于100,10000,1000000,… 这些形式。
不难归纳出如下结论:
4的幂一定是2的。
4的幂和2的幂一样,二进制位里只有一个是1,且是首位。但是,4的幂中的1总是出现在奇数位。
这里贴下 Kotlin 代码(非最佳实现):
fun isPowerOfFour(num: Int): Boolean {
if (num < 1){
return false
}
return (num.and(num - 1) == 0 && Integer.toBinaryString(num).length.and(1) == 1)
}
其他进阶操作
// 7. int 型最大值是什么?
System.out.println((1 << 31) - 1);// 2147483647, 注意运算符优先级,括号不可省略
System.out.println(~(1 << 31));// 2147483647
// 8. int 型最小值是什么?
System.out.println(1 << 31);
System.out.println(1 << -1);
// 9. long 型最大值是什么?
System.out.println(((long)1 << 127) - 1);
// 10. 整数n乘以2是多少?
n << 1;
// 11. 整数n除以2是多少?(负奇数的运算不可用)
n >> 1;
// 12. 乘以2的n次方,例如计算10 * 8(8是2的3次方)
System.out.println(10<<3);
// 13. 除以2的n次方,例如计算16 / 8(8是2的3次方)
System.out.println(16>>3);
// 14. 取两个数的最大值(某些机器上,效率比a>b ? a:b高)
System.out.println(b&((a-b)>>31) | a&(~(a-b)>>31));
// 15. 取两个数的最小值(某些机器上,效率比a>b ? b:a高)
System.out.println(a&((a-b)>>31) | b&(~(a-b)>>31));
// 16. 判断符号是否相同(true 表示 x和y有相同的符号, false表示x,y有相反的符号。)
System.out.println((a ^ b) > 0);
// 17. 计算2的n次方 n > 0
System.out.println(2<<(n-1));
// 18. 求两个整数的平均值
System.out.println((a+b) >> 1);
// 19. 从低位到高位,取n的第m位
int m = 2;
System.out.println((n >> (m-1)) & 1);
// 20. 从低位到高位.将n的第m位置为1,将1左移m-1位找到第m位,
//得000...1...000,n 再和这个数做或运算
System.out.println(n | (1<<(m-1)));
// 21. 从低位到高位,将n的第m位置为0,将1左移m-1位找到第m位,
//取反后变成111...0...1111,n再和这个数做与运算
System.out.println(n & ~(0<<(m-1)));
// 22 暂时木有了,想到再加~
4.Java 位运算趣味应用
1.只出现一次的数字
给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
解法:
解题思路其实很简单,就是上面提到的异或操作满足交换律和结合律,任何整数和自己异或的结果为 0,任何整数与 0 异或其值不变,我直接贴下 Kotlin 代码
fun singleNumber(nums: IntArray): Int {
var result = 0
nums.forEach {
result = result.xor(it)
}
return result
}
2.寻找缺失数字
给定一个包含 0, 1, 2, …, n 中 n 个数的序列(无序排列),找出 0 .. n 中没有出现在序列中的那个数。
示例 1:
输入: [0,1,3]
输出: 2示例 2:
输入: [9,6,4,2,3,5,7,0,1]
输出: 8
解法:
解题思路其实很简单,还是上面提到的异或操作满足交换律和结合律,任何整数和自己异或的结果为 0,任何整数与 0 异或其值不变,我直接贴下 Kotlin 代码
fun missingNumber(nums: IntArray): Int {
var sum = nums.size
nums.forEachIndexed { index, i ->
sum = sum.xor(index)
sum = sum.xor(i)
}
return sum
}
3.额好吧暂时只想到两条,想到别的会更新文章加上
未完待续…
4.Java 位运算操作库
我们以 Java 标准库的 Integer 类为例,emmm…Java 标准库里面封装好的所有位操作方法还请大家自行翻阅 Java 标准库文档哈!我在这里只举几个标准库里面容易被大家忽略的有用方法。
主要有三个:
//返回指定int值的二进制补码表示形式中的值为1的位的个数。
Integer.bitCount(int i)
//使用
//17的二进制补码表示是10001,可以看到里面共有两个位值为1
Integer.bitCount(17) == 2//true
//把指定int值的二进制补码除了值为1的最高位之外的所有非最高位值为1的位——置值为0返回。
Integer.highestOneBit(int i)
//使用
//十进制数 36 的二进制补码表示为100100,应用 highestOneBit 操作后返回二进制补码100000,即十进制的32
Integer.highestOneBit(36) == 32//true
//把指定int值的二进制补码除了值为1的最低位之外的所有非最低位值为1的位——置值为0返回。
Integer.lowestOneBit(int i)
//使用
//十进制数 36 的二进制补码表示为100100,应用 lowestOneBit 操作后返回二进制补码100,即十进制的4
Integer.lowestOneBit(36) == 4//true
5.总结
主要内容以上,不知在座各位看完本文后内心作何感想,我来帮大家总结下吧:位运算操作是很多编程语言的基础组成部分,它初看起来好像没什么卵用,很多人在学习编程时初次接触位运算操作内心可能也是比较懵逼,不知道这东西能用来干嘛,索性就一笔带过了!不过从上面我们的总结来看,位运算操作因为所处位置比较接近计算机底层,所以合理利用,往往能达到四两拨千斤的效果!单从android编程来看,我们在加密算法还有网络包处理等业务上使用位运算的频率还是很高的,更别提Intent中的那些种类繁多的Flag以及View布局里那些难以一言道尽的常量值了(比如Gravity里各个用位或运算符拼接的常量值),因此学好这方面的基础知识还是很重要的。
当然,还有同样很重要的编程技术面试,位运算也是一大考察点呢!
另外,作者水平有限,文中的技术错误肯定不敢说没有(虽然写这篇文章我还专门留时间检查了几遍),另外相关知识点总结的未必全面,文中恐有疏漏,欢迎各位同行评论区不吝赐教!
另
作者 github:点这儿
欢迎 follow
未完待续…