什么是位运算?
程序中的所有数在计算机内存中都是以二进制的形式储存的。位运算说穿了,就是直接对整数在内存中的二进制位进行操作。比如,and运算本来是一个逻辑运算符,但整数与整数之间也可以进行and运算。举个例子,6的二进制是110,11的二进制是1011,那么6 and 11的结果就是2,它是二进制对应位进行逻辑运算的结果(0表示False,1表示True,空位都当0处理):
110 AND 1011 ———- 0010 –> 2
由于位运算直接对内存数据进行操作,不需要转成十进制,因此处理速度非常快。当然有人会说,这个快了有什么用,计算6 and 11没有什么实际意义啊。这一系列的文章就将告诉你,位运算到底可以干什么,有些什么经典应用,以及如何用位运算优化你的程序。
引言
这个还真是基础中的基础,如果你跟我一样之前没有好好学习过Java基础语法,这块对你来说应该是一个懵逼点吧。不谈底层什么的,单单从android编程来看,我们在加密算法还有网络包处理等业务上使用位运算的频率还是很高的,更别提Intent中的那些种类繁多的Flag了,因此学好这方面的基础知识还是很重要的
本系列的例子使用的是Kotlin的语法,跟Java相比还是有所区别,请对照参考
无符号和有符号
在计算机中,可以区分正负的类型称为有符号类型,没有正负类型的称为无符号类型。
一个字节为8位,从0开始算,那它的最高位就是第7位。同样2个字节最高位为第15位,4个字节最高位为第31位。不同长度的类型,最高位不同,但是都是最左边的那个。
无符号数中,所有的位都用于直接表示该值的大小;有符号数中最高位用于表示正负,0表示正数,1表示负数。因此同样一个字节,无符号数最大值为255,有符号数最大值为127。有符号数最大值计算完全跟无符号数一样,但是在负数范围内就不能用刚才那种计算方式了,在计算机中,负数除了最高位为1以外,还采用补码的形式,所以在计算中要对补码进行还原
值得注意:的是JAVA的原始类型里没有无符号整型,如果需要转成无符号类型,可以用ushr
原码、补码、反码
这个是高中就教过的知识,这里就不再做介绍
提醒一下,负数都是用补码参与运算的,得到的也是补码,需要减1取反获得原码。
位运算符
位运算主要在直接操控二进制数时进行使用,可以达到节约内存,使你的程序运行速度更快
Java定义了位运算符,可应用在整形(int)、长整型(long)、短整型(short)以及字符型(byte)等类型上。位运算符作用在所有的位上,并按位进行运算。Kotlin与之略有不同,它并没有提供特殊的操作符,只提供了中缀形式的表示方法,并且Kotlin只可用在Int和Long类型上,这点千万要记住
咱们来看一个例子
val a1 = 60 val b1 = 13 var c1 = -5 // 与 println(a1 and b1) // 或 println(a1 or b1) // 异或 println(a1 xor b1) // 按位取反 println(a1.inv()) // 左移 println(a1.shl(1)) // 右移 println(a1.shr(1)) // 无符号右移 println(a1.ushr(1))
先看看结果,然后我们再一个个的分析
12 61 49 -61 120 30 30
我们知道,Java中的Int是4个字节32位的,那么a1与b1、c1转换成二进制就应该是
val a1 = 60 // 0000 0000 0000 0000 0000 0000 0011 1100 val b1 = 13 // 0000 0000 0000 0000 0000 0000 0000 1101 var c1 = -5 // 1111 1111 1111 1111 1111 1111 1111 1011
随后就是7种中缀表达式的计算规则
and 如果对应位都是1,则结果为1,否则为0
or 如果对应位都是0,则结果为0,否则为1
xor 如果对应位值相同,则结果为0,否则为1
inv 按位翻转操作数的每一位,即0变成1,1变成0
shl 按位左移指定的位数,相当于乘以2的N次方。移掉的省略,右边缺失的位,用0补齐
shr 按位右移指定的位数,相当于除以2的N次方,移掉的省略,左边缺失的位,如果是正数则补0,若为负数,可能补0或补1,这取决于所用的计算机系统
ushr 按位右移指定的位数,移掉的省略,左边缺失的位,用0补齐
所以我们来看看按位计算的结果(省略掉高位重复的0或者1,只看低八位)
通过上述规则,我们就能明白计算结果是怎样得到的
“与”的结果就是00001100,即为12 “或”的结果就是00111101,即为61 “异或”的结果就是00110001,即为49 “非a1”的结果就是11000011,即为-61 “左移a1 1位”的结果就是01111000,即为120 “右移a1 1位”的结果就是00011110,即为30 “无符号右移a1 1位”的结果就是00011110,即为30
有一个很有趣的现象,对于一个Int类型的数值,无论你执行左移还是右移还是无符号右移,只要移动32位,效果跟没有移动一致。这是因为在JAVA进行移位运算中因为Int是占32位,进行移位的数就是32的模,所以当数值移动32位的时候就等于数值移动0位,也相当于没有进行移位。同理Long类型的移位,Long占8字节也就是64位,所以移位的数是64的模
应用举例
有一个位运算口诀大家可以记一下:
清零取反要用与,某位置一可用或
若要取反和交换,轻轻松松用异或
判断Int型变量a是奇数还是偶数
a1 and 1 = 0 // 偶数 a1 and 1 = 0 // 奇数
获取Int型变量的第K位(注:K从0开始依次由右往左,以下揭同)
a1 shr k and 1
将Int型变量的第K位清0
a1 and ((1 shl k).inv())
将Int型变量的第K位置1
a1 or (1 shl k)
平均值
(a1 and b1)+((a1 xor b1) shr 1)
不用temp交换两个整数
a1 = a1 xor b1 b1 = b1 xor a1 a1 = a1 xor b1
获取绝对值
val temp = c1 shr 31 (c1 + temp) xor temp (c1 xor temp) - temp
获取相反数
c1.inv()+1
Int转byte数组
val bytes = ByteArray(4) bytes[0] = (a1 and 0xFF).toByte() bytes[1] = (a1 shr 8 and 0xFF).toByte() bytes[2] = (a1 shr 16 and 0xFF).toByte() bytes[3] = (a1 shr 24 and 0xFF).toByte()
补零扩展和补符号位扩展
在看这个问题之前,我们先做一个小游戏
val byte1: Byte = -127 println(byte1) println(byte1.toInt()) println(byte1.toInt() and 0xff)
看官请猜猜看呢
还是我来公布答案吧
-127 -127 129
能解释一下为什么-127变成129吗?这个就牵扯到补零扩展和补符号位扩展了
之前我们知道,Java是没有无符号类型的,byte是8个字节而int是32个字节。在byte转int的时候,肯定要将8位补到32位。Java中的扩展方式是补符号位扩展,所以-127通过补符号位扩展之后还是-127,即11111111 11111111 11111111 10000001。如果采用补零扩展,相当于11111111 11111111 11111111 10000001 and 11111111,这个值就是00000000 00000000 00000000 10000001,也就是129了
为了加深记忆,我再留一道题给大家思考
println(0x100000000L + 0xcafebabe.toInt()) println(0x100000000L + 0xcafebabeL)
这两个表达式打印出来也不一样,现在你应该能明白为什么不一样了吧
总结一下
Java中只有有符号数。当byte扩展到short,int时,因为符号位是0,所以正数都一样,无论如何都是补零扩展;但负数补零扩展和按符号位扩展结果完全不同。
补符号位扩展,原数值不变。补零扩展,相当于把有符号数看成无符号数。
对于有符号数,默认采用符号位扩展。由小扩展到大时,需要用and 0xff这样方式来确保是按补零扩展的;而从大向小时,符号位自动无效,所以不用处理。如果是char类型,那么不管它将要被扩展成什么类型,都执行补零扩展