Java位运算
我们必须要了解的Java位运算(不仅限于Java)](https://www.cnblogs.com/zh94/p/16195373.html)
机器数
机器数分为无符号数和有符号数两种。
一个数在计算机中的二级制表现形式,叫做这个数的机器数。机器数是带符号的,在计算机用一个数的最高位存放符号,正数位0,负数为1。
例如:十进制中的“+3”,计算机字长为8位,转换成二进制就是00000011。
如果是“-3”,就是10000011。这里的00000011和10000011就是机器数
真值
因为计算机存储的第一位是符号位,所以机器数的形式值就不等于真正的数值。
例如上面的符号数10000011,其最高位1代表负,其真正的数值是-3,而不是形式值131(10000011转换成十进制等于131).
所以,为区别起见,将符号位的机器数真正数值称为机器数的真值。
例:0000 0001的真值=+000 0001=+1,1000 0001的真值=-000 0001=-1
原码、反码、补码
为了妥善的处理数据运算过程中符号位的问题,于是就产生了把符号位和数值位一起编码起来表示相应数的各种表示方法。例如原码、反码、补码、移码等。通常将未经编码的数称为真值,编码后的数称为机器数或机器码。
计算机当前所使用的机器数是采用的补码的方式。对于一个数,计算机使用一定的编码方式进行存储,原码,反码,补码是机器存储一个具体数字的编码方式。
原码
原码就是符号位上加上真值绝对值,即用第一位表示符号,其余位表示值,比如如果是8位二进制:
[+1]原 = 0000 0001
[-1]原 = 1000 0001
第一位是符号位,因为第一位是符号位,所以8位二进制的取值范围就是:
[01111111,11111111]
转换成真值后即:
[127,-127]
原码是人脑最容易理解和计算的表示方式。原码也可以理解为最原始的机器码
反码
反码的表示方法是:
正数的反码是其本身
负数的反码是在其原码的基础上,符号位不变,其余各位取反
[+1] = [00000001]原 = [00000001]反
[-1] = [10000001]原 = [11111110]反
可见如果一个反码表示的是负数,人脑无法直接直观的看出来它的数值,通常要将其转换成原码再计算
补码
补码的表示方法是:
正数的补码是其本身
负数的补码是在其原码的基础上符号位不变,其余各位取反,最后+1.(即在反码的基础上+1)
[+1] = [00000001]原 = [00000001]反 = [00000001]补
[-1] = [10000001]原 = [11111110]反 = [11111111]补
对于负数,补码的方式也是人脑无法直观看出其数值的,通常也需要转换成原码再计算其数值
互相转换、注意事项
注意:一个数在计算机中的二进制表示方式叫做机器数,而机器数是有符号的,我们将对应的最高位存放符号,0表示正数,1表示负数。所以机器数并不等于二进制。
因此我们引入了真值的概念,例如:符号数10000011,在十进制中对应的值是131,但是在机器数中,由于最高位1表示负数,所以真正的数值是-3,而不是131.
符号数:10000011,转换成数值需要先消除符号位的影响,将其调整为正数:00000011,再将正数转换为十进制3,然后再加上最初的符号位表示负数,所以为-3。
需知道的是,二进制仅是以2为基数的计数方式而已,在二进制中无法区分正数与负数的。而如果想区分正数与负数那么必须在二进制的最高一位中用0和1来表示符号来依此表示正负,那么此时最高位的0和1已经不是二进制的一种计数方式了,而只是一种符号的标识,该符号的标识则不能参与二级制的计算的。
当我们用最高位中的0和1来表示正负时,此时的二进制数已经不再是符合二进制的规则的数了,而是机器数。
所以,也只有机器数才可以标识正负。而机器数所对应得到的结果则是真数。
如果此时抛出来一个问题:10000011 转换为对应的10进制,那么对应的结果则是131,但如果是转换为真数,则是-3。同样的,如果是将00000011 转换为对应的10进制,则是3,而如果转换为真数,则也是3。
所以当我们看到一个以8位数所表示的二进制数时,则一定要确认该二进制数是表示机器数,还是二进制数?是转换为10进制数,还是真数。其中最大的区别则是,最高的符号位到底是参与二进制的运算,还是仅仅表示符号位。
机器数转真数:
10000011 -> 消除最高位1的影响先转为正数 -> 00000011 -> 再将正数以二进制的方式转为十进制 -> 3 -> 此时再将最初的服啊后添加回来 -> 3调整为 -3。
二进制数转十进制数
10000011 -> 无需消除最高位1的影响直接转换为正数 -> 10000011 -> 再将正数以二进制的方式转为十进制 -> 131 -> 无需添加符号位 -> 仍然是131。
为什么补码才是计算机的真正计算方式
计算机可以又三种编码方式表示一个数,对于正数,三种编码方式的结果都相同:
[+1] = [00000001]原 = [00000001]反 = [00000001]补
对于负数原码, 反码和补码是完全不同的
[-1] = [10000001]原 = [11111110]反 = [11111111]补
前面说的原码才是人脑直接识别并使用的计算方式,为何还有反码和补码呢?
首先,因为人脑可以知道第一位是符号位,再计算的时候我们会根据符号位,选择对真值区域的加减。
但对于计算机,加减乘除已经是最基础的运算,要设计的尽量简单,计算机辨别"符号位"显然会让计算机的基础电路设计变得十分复杂!于是人们想出了将符号位也参与运算的方法。我们知道,根据运算符减去一个正数等于加上一个负数,即:1-1=1+(-1)=0,所以机器可以只有加法,而没有减法,这样计算机运算的设计就更简单了。
于是人们开始探索,将符号位参与运算,并且只保留加法。首先看原码:
计算十进制的表达式:1-1=0
1 - 1 = 1 + (-1) = [00000001]原 + [10000001]原 = [10000010]原= -2
如果使用原码表示,让符号位也参与计算,显然对与减法来说,结果是不正确的。这也是为什么计算机内部不使用原码表示一个数。
为了解决原码做减法的问题,出现了反码:
计算十进制的表达式:1-1=0
1 - 1 = 1 + (-1) = [0000 0001]原 + [1000 0001]原 = [0000 0001]反 + [1111 1110]反
= [1111 1111]反 = [1000 0000]原 = -0
发现用反码计算减法,结果的真值部分是正确的。而唯一的问题就出现在"0"这个特殊的数值上,虽然人们理解上+0和-0是一样的,但是0带符号是没有任何意义的。而且会出现"[00000000]原"和"[10000000]原"两个编码表示0。
于是补码的出现,解决了0的符号问题,以及两个编码的问题:
1 - 1 = 1 + (-1) = [0000 0001]原 + [1000 0001]原 = [0000 0001]补 + [1111 1111]补
= [0000 0000]补 = [0000 0000]原
这里说明一下,二进制想加:0000 0001 + 1111 1111 = 1 0000 0000 ,但是由于是8位数,所以最终的值是0000 0000。
这样0用[0000 0000] 表示,而以前出现的问题-0则不存在了,而且可以用 [1000 0000]表示-128:
(-1) + (-127) = [1000 0001]原 + [1111 1111]原 = [1111 1111]补 + [10000 0000]补
= [1000 0000]补
由于我们使用原码表示正数时,最大值为:011111111,最小值为:11111111,所以直接转换为对应的十进制后结果为:127,-127。
而此处使用补码后,由于补码的规则是首位不变,其他反转,并+1。所以(-1)+(-127)刚好为-128
使用补码,不仅仅修复了0的符号以及两个编码的问题,而且还能够多表示一个最低数. 这就是为什么8位二进制, 使用原码或反码表示的范围为[-127, +127], 而使用补码表示的范围为[-128, 127]。
因为机器使用补码, 所以对于编程中常用到的32位int类型, 可以表示范围是: [-2的31次方, 2的31次方 - 1] 因为第一位表示的是符号位.而使用补码表示时又可以多保存一个最小值。
Amazing,我们在上面最初使用原码进行加法运算时,由于我们人脑还需要先判断一下最高位的符号后,才能进行二进制运算,然后再添加上对应的符号位。而采用补码后,直接将对应的符号位也参与运算,将补码的数值直接相加,得到的竟然刚好也就是二进制转换后的结果。这样一来,计算机的基础电路设计就可以更加简单,而无需关注符号位的问题,仅需要按照二进制的加法法则执行即可。简直完美。所以这也是补码作为计算机的真正计算方式的原因之一!
位运算
概念
百度百科:
程序中所有数在计算机内存中都是以二进制的形式储存的。位运算就是直接对整数在内存中的二进制位进行操作
百度百科中所给的解释具有歧义,按照百度百科的解释,直接对整数在内存中的二进制位进行操作就是位运算的话,那么使用二进制数进行算法运算(+,-,*,/)岂不是也属于位运算
维基百科:
位运算是程序中对位模式或二进制数的一元和二元操作。在许多古老的微处理器上,位运算比加减运算略快,
通常位运算比乘除法运算要快很多。在现代架构中,情况并非如此:位运算的处理速度通常
与加减运算相同(仍然快于乘法运算)。
维基百科中针对运算的概念相对合理,通过维基百科中的概念我们可以明显的区分到,位运算是与加减乘除这些算术运算符是不同的。不同的CPU针对位运算的操作是较快于(乘/除)法运算的
所以这也才是我们需要了解位运算的真正原因,那就是CPU处理器针对位运算符的计算是快于算数运算符的!在特定的编码环境下使用位运算的效率则是远远大于算数运算的!
网络上针对位运算较多的内容解释是:位运算是直接对整数在内存中的二进制位进行操作,所以位运算更加节省内存、
提高运算效率等等的。其实这是很不严谨的说法,很容易误导大家对位运算的理解,因为所有的整数最终在计算机中
都是二进制数,那么所有对整数的运算岂不是都是位运算?当然不是啦。且,位运算真正快的原因也并不是因为节省
内存,而主要是因为CPU对位运算的支持!和内存并没有较大关联。
为什么位运算CPU执行效率更快
程序中的基本运算包含:
-
算术运算符:加、减、乘、除、取余
-
按位运算:按位或”|“、按位与”&“、按位取反”~“、按位异或”^“
-
移位运算:左移x<<k;右移x>>k
其中按位运算和移位运算均属于位运算的范畴。
位运算的具体执行逻辑:
&与运算的运算规则是:两个位都位1时,则结果为1。
如:3&5即0000 0011&0000 0101=0000 0001,因此3&5的值为1。
根据与运算的规则可知,位运算的整体执行逻辑是较为简单的,更多时进行位数的比较,从而得到一个结果,这种较为简单的运算逻辑,则对于CPU来说,在电路设计中则会更加简单许多,以下为与运算符所设计到的CPU电路图
而对于一个除法来说,在CPU中所对应的电路图设计则是这样的:
可以看到,整个CPU电路图的设计复杂了不只是一个层级,所以这也就是为何位运算比我们人常用的算术运算更快的直接原因了。因为对整个CPU的执行逻辑来说从设计层面就复杂了很多。
位运算符
首先需知道的是,计算机中执行位运算符,肯定是采用补码的方式进行的位运算,所以对于真值位负数的情况下,必须先转为补码才能计算
符号 | 描述 | 运算规则 |
---|---|---|
& | 与 | 两位都为1时,结果才为1 |
| | 或 | 两个位都为0时,结果才为0 |
^ | 异或 | 两者相同为0,相异为1 |
~ | 取反 | 0变1,1变0 |
<< | 左移 | 各二进位全部左移若干位,高位丢弃,低位补0 |
>> | 右移 | 各二进位全部右移若干位,对无符号数,高位补0。各编译器处理方式不一样,有的补符号位(算数右移),有的补0(逻辑右移) |
&与 运算符
运算规则:两位同时为1,结果才为1,否则结果为0.
0&0=0 0&1=0 1&0=0 1&1=1
例如:2&-2
0 0 0 0 0 0 1 0
& 1 1 1 1 1 1 1 0 0 0 0 0 0 0 1 0 运算结果:2&-2=2
&与运算符的用途:
根据与运算的计算特性,我们常用的使用方式有:
1、判断奇偶数
我们知道,按照二进制和十进制(除二取余)的换算方式,如果是偶数的情况下换算为二进制后末位必然是0,如果是奇数末位则是1。比如:2->10,3->11,50->110010,51->110011
所以我们按照与运算的运算规则,使用a&1==1表示位奇数,a&1==0则表示位偶数
如:
121&1=1111001 & 0000001=00000001
2&1=10&01=00
使用与运算的方式,则完全可以代替掉:if(a%2==0)来判断奇偶数的方式,且位运算由于CPU的支持,执行效率也更高。
2、清零
如果想将一个单元清零,即其全部二进制位为0,只要与0进行按位与运算,就可达到清零效果。
3、取一个数的指定位
比如取数X=1010 1110高4位只需要另外找一个数,令Y的高4位为1,其余位为0,即Y=1111 0000,然后将X和Y进行与运算,即(X&Y=1010 1110&1111 0000=1010 0000)
即可得到X的指定高4位。
如果想获取X的低4位的数,则将Y的低4位为1,其余位为0即可,(X&Y=1010 1110&0000 1111=0000 1110)便可以得到X的指定低4位。
|或 运算符
运算规则:两各位都为0时,则结果为0。否则为1。
0|0=0 0|1=1 1|0=1 1|1=0
总结:参加运算的两个对象只要有一个为1,其值为1。
例如:2|-2=-2
0 0 0 0 0 0 1 0
| 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 0 运算结果 2|-2=-2
|或 运算符的用途
1、将一个数据的某些位设置为1
比如将数X=1010 1110的低4位设置为1,只需要另外找一个数Y,令Y的低4位为1,其余位为0,即Y=0000 1111,然后将X与Y进行按位或运算(X|Y=1010 1111)即可得到。
同样的,使用&运算符则可以方便的将某些位设置为0,如上述X&Y,将X的低四位设置为0,则X&Y = 1010 1110 & 1111 0000 =1010 0000。
以上则说明 & 和 | 灵活运用,其实是可以达到相同效果的。但实际使用中则并不然,首先对于上述低4位设置为1的场景,我们只需要找一个Y的数,令Y的低4位为1,其余位为0,这样一个数是很好找的,是一个固定的数,比如:15。转换为二进制后为1111。
但如果使用 & 运算符来面对这个场景,则需要找一个Y,Y的低四位为0,其余位置为1,这样一个数则很难找,并且随着位数的不同,值也是不断变换的,比如:1111 0000=240,但如果是12位数,1111 1111 0000=4080。所以如果使用 & 运算符来在该场景下则是没有 | 运算符更加方便的。
尽管 & 和 | 的规则相反,可灵活变更,但针对特定场景下,还是使用特定的运算符效果更佳
^异或 运算符
运算规则:两个位相同为0,相异为1.
0^0=0 0^1=1 1^0=1 1^1=0
例如:2^-2
0 0 0 0 0 0 1 0
^ 1 1 1 1 1 1 1 0
1 1 1 1 1 1 0 0
运算结果:2^-2=-4
^异或 运算符的用途
1、交换两个数
a=a^b; //a=a^b b=a^b; //b=(a^b)^b=a^0=a a=a^b; //a=(a^b)^(a^b^b)=0^b=b
交换两个数的原理,即上面注释所写内容
不适用位运算的方式交换两个数,则需要定义一个中间变量C,来承接其中一个数的交换,对于Java来说定义一个新的int类型的变量C,则表示内存中需开辟一个4字节的空间。
所以根据服务特性来选择合适的方式即可,对内存使用率有较强要求则使用位运算,没要求则都可以
static void swap(int a,int b){int c=a; a=b; b=c;} static void swapBit(int a,int b){a=a^b; b=a^b; a=a^b}
~取反 运算符
运算规则:0变1,1变0.
变量名称 | 类型 | 大小 | 二进制 | 按位取反表示 | 按位取反二进制 | 取反大小 |
---|---|---|---|---|---|---|
i | byte(8位) | 127 | 011111111 | ~01111111 | 10000000 | -128 |
j | byte(8位) | -1 | 11111111 | ~11111111 | 00000000 | 0 |
<<左移运算符
运算规则:将一个运算对象的各二进制位全部左移若干位(左边的二进制位丢弃,右边补0).
设a=15,即二进制数00001111,左移两位得00111100,即十进制60。
设a=-46,补码后为:1010 1110,a=a<<2将a的二进制位左移2位、右补0,即得a=1011 1000转换为真值后为-56。
设a=110,补码后为:0110 1110,a=a<<2将a的二进制位左移2位、右补0,即得a=1011 1000,转为真值后为184。
>>右移运算符
运算规则:将一个运算对象的各二进制位全部右移若干位,正数左补0,负数左补1,右边舍弃。
设a=16,补码后为00010000,a=a>>2将a的二进制位右移2位,左边补0,即得a=00000100,转换为真值后为4。
设a=-16,补码后为11110000,a=a>>2将a的二进制位右移2位,左边补1,即得a=11111100,转换位真值后位-4。
结论:右移运算符,操作数每右移1位,相当于该数除以2。
Java中位运算应用
1、JDK中线程池ThreadPoolExecutor的实现当中使用Integer类型(4字节,32位)其中高3位保存线程池状态,而低29位保存线程池内有效线程数量。
2、比如JDK的HashMap中使用位运算的方式初始化容量的数值,快速的转换为2的n次幂。以及计算key的hash时,根据key的hashCode结果,再将hashCode的高16位和低16位通过位运算的方式进行混合,以此降低hash碰撞的概率等等。
......
补充说明:
在Java当中的位运算,是只针对int类型和Long类型有效(java中,一个int类型的长度始终是32位,也就是4个字节,它操作的都是该数的二进制数,Long则是64位,8个字节),对于byte、char、short当位这三个类型时,JVM会先把他们转换为int后再进行操作。
使用toBinaryString() 可以将对应的十进制转为对应的补码。
System.out.println(Integer.toBinaryString(10));
//1010 System.out.println(Integer.toBinaryString(-10));
//11111111111111111111111111111101 System.out.println(Long.toBinaryString(10));
//1010 System.out.println(Long.toBinaryString(-10));
//1111111111111111111111111111111111111111111111111111111111110110
小结:
1. 二进制的最高位是符号位:0表示正数,1表示负数
2. 正数的原码、反码、补码都一样
3. 负数的反码=它的原码符号位不变,其他位取反
4. 负数的补码=它的反码+1;负数的反码=负数的补码-1
5. 0的反码,补码都为0
6. java没有无符号数,换言之,Java中的数都是有符号数
7. 在计算机运算的时候,都是以补码的方式运算的
8. 我们看运算结果的时候,需要看他的原码
感谢您的阅读,如果您觉得阅读本文对您有帮助,请点一下“推荐”按钮。本文欢迎各位转载,但是转载文章之后必须在文章页面中给出作者和原文连接。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 周边上新:园子的第一款马克杯温暖上架
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
· DeepSeek如何颠覆传统软件测试?测试工程师会被淘汰吗?
· 使用C#创建一个MCP客户端