咱们最开始学习java的时候知道,byte类型的数据占了8个bit位,每一个位上或0或1,左边第一位表示符号位,符号位若是为1表示负数,为0则表示正数,所以要推算byte的取值范围,只须要让数值位每一位上都等于1便可。java
咱们来用咱们的常规思惟来分析下byte类型的取值范围:数据库
图1性能
若是按照这种思路来推算,七个1的二进制数转换为十进制是127,算上符号位,取值范围应为:-127~+127,但事实上咱们知道,byte的取值范围是-128~127,这里先打个问号,接着往下看。学习
如今让咱们计算下byte类型的7加上byte类型的-2是多少:spa
图23d
诶?跟咱们预想的不同,由于咱们是知道7和-2的和应该是5才对,结果应该表示为:00000101,但事实上经过图2的结果来看确实跟预想的不同,因此计算机在作计算的时候,确定不是表面上的符号位+数值位的方式进行的计算的。code
咱们先来看下定义:blog
👉 原码定义:符号位加后面的数值,好比图2里的00000111和10000010都是原码,原码比较简单,就是咱们在上面单纯理解上的原值。it
👉 反码定义:正数的反码就是它的原码,负数的反码符号位不变,其他数值位所有按位取反,例如:class
00000111的反码:00000111
10000010的反码:11111101
👉 补码定义:一样的,正数的补码仍然等于它的原码自己,负数的补码等于它本身的反码+1,例如:
00000111的补码:00000111
10000010的补码:11111110
🌴 总结:正数的原码、反码、补码彻底一致,负数的反码等于它原码的数值位按位取反,负数的补码等于它的反码+1
如今让咱们用反码的方式来计算下图2中的式子:
图3
利用数值的反码计算出的结果已经很接近正确答案了,+4的反码等于它的原码,如今只须要让它+1就是正确答案,还记得补码的定义吗?负数的补码等于它的反码+1,那如今让咱们用补码作下计算试试?
图4
ok,咱们发现,用它们的补码作加法,获得的数值就是咱们想要的正确答案,事实上,计算机并无减法运算器,全部的减法运算,都是以一个正数加上一个负数的形式来交给加法运算器计算的,因为负数的符号位为1,虽然咱们人是知道它的含义,可是做为计算机,它是不知道第一位是符号位的,它要作的就仅仅是让两个数相加而已,正是由于如此,咱们才不能简简单单保存负数,经过图4咱们知道,两个数的补码相加,能够获得一个准确的数值。
再举个相加结果为负数的例子,让两个负数相加:
图5
若是结果为负数的话,也是适用的,只是它仍然是以补码的形式存放的,须要转成原码才符合咱们人的理解方式。
如今回到上面留下的问题,为何byte的取值范围是-128~127呢?
咱们以前按照图1里的理解,理所应当的觉得它应该是-127~127的范围,那是由于咱们按照图1的理解方式,数值就是以符号位+数值位的方式理解的(也就是按照原码的方式理解的),可是你能够想一下,若是按照图1那种理解方式,是否是会存在两个0值呢?
即:10000000和00000000,+0和-0;
其次若是站在机器角度上来讲,全部的负数都很大,至少要比全部正数大,由于负数的最高位也就是符号位都是1,显然这是不对的,经过本节咱们知道了,全部的数均经过本身的补码完成计算,若是将最后获得的结果转成原码,就是咱们人眼能够理解的最终值(符号位+数值位),若是如今利用补码的方式作理解,符号位为0的数没啥好说的,天然取值区间为:0~127,可是符号位为1的负数呢?负数就存在一个特殊值(也就是咱们以前片面理解的-0):10000000,若是按照原码理解它是-0,但咱们前面说过,计算机里全部数字,都是以补码的方式参与运算的,而负数的补码不等于其原码,这个10000000在计算机里显然是某个负数的补码,那么问题就变的简单多了,即10000000是谁的补码呢?答案是:-128,这也是为何负数的取值范围会比正数多一个的缘由,byte类型如此,其它类型也是如此,好比int型的负数取值也比正数多1。
这一块的定义要清晰,对理解后面的位运算会有很大的帮助。
与运算符号:&
与运算特色:1&1=一、1&0=0、0&1=0、0&0=0
如今咱们来举一个例子:
图6
让咱们再来试试负数:
图7
跟与运算的运算方式一致,只不过规则不太同样:
或运算符号:|
或运算规则:1|1=一、1|0=一、0|1=一、0|0=0
异或运算符号:^
异或运算规则:1^1=0、1^0=一、0^1=一、0^0=0
取反符号:~
即一个数对本身取反,例如:
某个数字a的二进制为: 1010110
则~a为: 0101001
左移运算符:<<
例如:
图8
位运算越界&数位抛弃:
图8中的116的二进制数的数值位为7位,符号位为0,此时若是左移超过24位,就会出现负数,为何会这样?由于java中的位移越界时,java会抛弃高位越界部分,咱们知道java里int类型的第一位是符号位,若是符号位是1,则表示其为负数,如今将数值位占7bit符号位为0的116左移24位,就会出现下方结果:
01110100000000000000000000000000
正好31位占全,顶至符号位,低位补0,咱们称24为116的不越界的最大左移值,若超出这个值,就会越界,好比左移25位:
11101000000000000000000000000000
显然左移25位后会把数值位的1移动到符号位,这时它表示为一个负数的补码。根据这个规则,咱们若是让其左移28位,则值为:
01000000000000000000000000000000
也就是十进制的1073741824,即:116 << 28 = 1073741824,那若是越界过多呢?好比int型的数据,左移32位:116 << 32 = 116
会发现,若是左移本身位数同样多的位数,那么这个数就等于它自己,所以运算符合如下规则:
设x为被位移值,y为本次位移的位数,z为x所属类型的最大存储位数:
x << y = x << (y%z)
若是是int型(32位,long型就用64代入计算),符合以下规则:
116 << 4 = 116 << (4%32) = 116 << 4 = 1856
116 << 32 = 116 << (32%32) = 116 << 0 = 116
116 << 36 = 116 << (36%32) = 116 << 4 = 1856
有符号右移运算符:>>
无符号右移运算符:>>>
例如:a >> b表示a右移b位,跟上面的左移例子同样,右移也会有越界问题,只是右移越界是从右边开始抛弃越界部分的,右移操做有符号位干扰,若是是正数右移,无此干扰项,由于符号位本就是0右移不会影响值的准确性,但若是是负数,第一位是符号位,且值为1,右移就有影响了,如今仍然以116为例:
正数右移:
图9
上述是正数,右移无影响,可是负数,这里以-116为例,咱们知道负数在计算机里是以补码的形式存储的,因此图里直接用-116的补码作运算,位移过程以下:
图10
你会发现右移跟左移不同,左移是不用担忧本身符号位存在“补位”问题的,可是右移存在,如图中-116右移4位后,左边第一位,也就是符号位,就面临着补位的问题,那我如今是该补1呢,仍是补0呢?这也就是为何右移操做会存在有符号右移和无符号右移两种移动方式:
☘️ 有符号右移:依照原符号位,若是原符号位是1,那么图4里须要补位的空位所有补1,若是原符号位为0,则所有补0
☘️ 无符号右移:无视原符号位,所有补0
如今让咱们用有符号的方式将-116右移4位,即-116 >> 4,按照有符号的规则,补位符合原符号位,则右边4位所有补1:
图11
获得的仍然是个负数,它仍然是一个补码,图里展现不开,它的结果为:11111111111111111111111111111000,经转换可知它是-8的补码,即:-116 >> 4 = -8
如今再试试用无符号右移,根据无符号的特色,右移后的前四位无脑补0:
图12
图里展现不开,它的结果为:00001111111111111111111111111000
可见它是个正数,转换成十进制为:268435448,即:-116 >>> 4 = 268435448
最后说一下,跟左移同样,右移里不论是有符号仍是无符号,也符合取余的方式,计算出位移的最终位数:
-116 >> 4 = -116 >> (4%32) = -116 >> 4 = -8
-116 >> 32 = -116 >> (32%32) = -116 >> 0 = -116
-116 >> 36 = -116 >> (36%32) = -116 >> 4 = -8
了解完位运算,来看一个比较实际的问题,看下面的代码:
long a = 8934567890233345621L; int b = (int) a; //b的值为-1493678507
最终b的值是一个负数,这是因为long型64位,让int型强行接收,会出现位溢出的问题,这个流程以下:
图13
位运算的性能是很是好的,相比运算流程,计算机更喜欢这种纯粹的逻辑门和移动位置的运算,但位运算在日常的业务代码里并不太常见,由于它的可读性不太好,可是咱们仍然能够利用位运算来解决一些实际项目里的问题。
好比用来表示开关的功能,好比需求里常常有这种字段:是否容许xx(0不容许,1容许),是否有yy权限(0没有,1有),是否存在zz(0不存在,1存在)
上面只是举例,相似这种只有两种取值状态的属性,若是当成数据库字段放进去的话,太过浪费,若是以后又有相似的字段,又得新增数据库字段,为了只有两种取值的字段,实在是不太值得。
这个时候何不用一个字段来表示这些字段呢?你可能已经猜到要怎么作了:
图14
顶一个int型或者long型的字段,让它的每个二进制位拥有特殊含义便可,而后按照位运算将其对应的位置上的数值变成0或1,那如何将某个数的二进制位第x位上的数值变成1或0呢?其实这在位图结构里常常用到,就是利用1这个特殊的值做位移运算后再与原值进行位运算,让咱们看下这个过程:
把一个数的第2位的字符变成1,如今假设这个数初始化为0,int型,咱们把它当成二进制展现出来:
图15
如今如何把这个数的第二位变成1呢?目前是这样作的:
0 | 1 << 1
即原值跟1左移1位后的值做或运算,先来看看1 << 1的结果:
图16
而后拿着图16的结果,跟原数(也就是0)进行或运算:
图17
能够看到,原数的第二位已经被置为1了,它的十进制对应2,其它位的数置为1也大同小异,例如,如今让第6位也变成1只须要:
2 | 1 << 5
即拿着原值(如今为2)跟1左移5位后的数作或运算,这个流程以下:
图18
看完了把某个位置的数值置为1,那如何把某位设置为0呢?咱们如今把图18里的结果的第6位从新置回0,目前的作法为:
34 & ~(1 << 5)
即拿着原值(通过上面几步的运算,如今值为32)跟1左移5位按位取反后的数作与运算,来看下这个流程:
图19
通过上面的流程,就能够把原值的第6位变成0了。
那么咱们知道了让一个数的二进制位的某位变成0或1的方法,那如何知道一个数的某位上到底是0仍是1呢?毕竟咱们业务代码须要知道第几位表明什么意思而且获取到对应位置上的值。
假如我如今想知道十进制int型数34的第6位是0仍是1,写法以下:
34 >> 5 & 1
即让原值(34)右移5位后跟1作与运算,来看下这个流程:
图20
由图能够看出,想要知道一个数的第几位是1仍是0,只须要将其对应位置上的值“逼”到最后一位,而后跟1相与便可,若是对应位置上的值是0,那么与1相与后的结果必定为0,反之必定为1.
☘️ 总结
到这里已经说完了为何要用一个数表示那么多开关,以及如何给一个开关位设置对应的开关值,以及如何找到对应开关位的值,有了这些操做,咱们不再须要为这种只有0和1取值的字段新增数据库字段了,由于一个int型的数字,就能够表达32个开关属性,若是超了,还能够扩成64位的long型~