一、 相关基础概念
在开始 Java 位运算的知识之前,我们先来了解几个基础的概念,机器数,真值,原码,反码,补码。
1.机器数
我们知道无论是代码还是数值,在计算机中最后都转换成以二进制的形式存在的,而一个数值在计算机中的二进制表示形式,就是这个数的机器数。机器数是有符号位的,在计算机中用一个二进制数的最高位存放符号,正数为 0,负数为 1,如下实例(按原码表示):
十进制的+5,计算机字长为 8 位,其二进制就是:00000101
十进制的-5,计算机字长为 8 位,其二进制就是:10000101 (这里用的是原码)
其中 00000101 和 10000101 就是机器数
2.真值
由于机器数的第一位是符号位,所以其形式值就不等于其真值的数值,也就是说 10000101 表示的是-5 而不是133(10000101 的十进制是 131,前提是不算最高位为符号位),因此-5才是机器数的真值。
3.原码
原码是一种计算机中对数字的二进制定点表示方法。原码表示法在数值前面增加了一位符号位(即最高位为符号位):正数该位为 0,负数该位为 1,其余位表示数值的大小。
[+5]= [00000101](原码)
[ - 5]= [10000101](原码)
因为第一位是符号位,因此 8 位二进制的取值范围就是[1111 1111,0111 1111]也就是[-127,127]。
4.反码
反码是数值存储的一种,但是由于补码更能有效表现数字在计算机中的形式,所以多数计算机一般都不采用反码表示数,反码的表示方法如下:
正数的反码是其本身。
负数的反码是在其原码的基础上,符号位不变,其余各个位取反。
[+5]= [00000101](原码)= [00000101](反码)
[ - 5]= [10000101](原码)= [11111010](反码)
5.补码
在计算机系统中,数值一律用补码来表示和存储。原因在于,使用补码,可以将符号位和数值域统一处理。同时,加法和减法也可以统一处理。补码的表示方法是:
正数的补码就是其本身
负数的补码是在其原码的基础上,符号位不变,其余各位取反, 最后 + 1。(即在反码的基础上 + 1)
[+5]= [00000101](原码)= [00000101](反码)=[00000101](补码)
[ - 5]= [10000101](原码)= [11111010](反码)=[11111011](补码)
6.小结
计算机中的符号数有三种表示方法,即原码、反码和补码。三种表示方法均有符号位和数值位两部分,符号位都是用 0表示“正”,用1表示“负”,而数值位,三种表示方法各不相同。而在计算机系统中,数值一律用补码来表示和存储。
二、 Java位运算
位移操作:(只针对 int 类型的数据有效,Java中,一个 int 的长度始终是 32 位,也就是 4 个字节,它操作的都是该整数的二进制数),也可作用于以下类型,即 byte,short,char,long(它们都是整数形式)。当为这四种类型时,JVM先把它们转换成 int 型再进行操作。
7.左移(<<)
m << n 的含义:把整数 m 表示的二进制数左移 n 位,高位移出 n 位都舍弃,低位补 0。
5 << 2 :把十进制的数值 5 左移两位,按如下步骤计算:
把 5 转位 16 位的二进制机器数:00000000 00000000 00000000 00000101
按左移原理,将二进制数左移两位:00000000 00000000 00000000 00010100
左移后结果为:20
5 << 29:把十进制的数值 5 左移 29 位,按如下步骤计算,
把 5 转位 16 位的二进制机器数: 00000000 00000000 00000000 00000101
按左移原理,将二进制数左移 29 位:10100000 00000000 00000000 00000000
左移后高位是1,结果显然是负数。
小结:m << n 即在数字没有溢出的前提下,对于正数和负数,左移 n 位都相当于 m 乘以 2 的 n 次方。
8.右移(>>)
m >> n 的含义:把整数 m 表示的二进制数右移 n 位,m 为正数,高位全部补 0;m 为负数,高位全部补 1。实例如下:
5 >> 2 :把十进制的数值 5 右移两位,按如下步骤计算,
把 5 转位 16 位的二进制机器数:00000000 00000000 00000000 00000101
按右移原理,将二进制数左移两位:00000000 00000000 00000000 00000001
右移后结果为:1
-5 >> 2:把十进制的数值 -5 右移两位,按如下步骤计算,
把 - 5 转为 16 位的二进制机器数:11111111 11111111 11111111 11111011
按右移原理,将二进制数右移两位:11111111 11111111 11111111 11111110
右移后结果为:- 2
小结:m >> n 即相当于 m 除以 2 的 n 次方,得到的为整数时,即为结果。如果结果为小数,此时会出现两种情况:
如果 m 为正数,得到的商会无条件的舍弃小数位;
如果 m 为负数,舍弃小数部分,然后把整数部分加+1得到位移后的值。
9.无符号右移(>>>)
m >>> n:整数 m 表示的二进制右移 n 位,不论正负数,高位都补 0。实例如下:
5 >>> 2 :把十进制的数值 5 右移两位,按如下步骤计算:
把 5 转为 16 位的二进制机器数:00000000 00000000 00000000 00000101
按右移原理,将二进制数左移两位:00000000 00000000 00000000 00000001
右移后结果为:1
-5 >>> 2:把十进制的数值 -5 右移两位,按如下步骤计算:
把 -5 转为 16 位的二进制机器数:11111111 11111111 11111111 11111011
按右移原理,将二进制数右移两位:00111111 11111111 11111111 11111110
右移后结果为正数。
10.按位非操作(~)
~ 按位取反操作符,对每个二进制位的内容求反,即1变成0,0变成1。实例如下
把 -5 转为 16 位的二进制机器数:11111111 11111111 11111111 11111011
~(-5) 取反结果:00000000 00000000 00000000 00000100
转为十进制,结果为:4
11.按位与操作(&)
& 位与操作符,对应的二进制位进行与操作,两个都为 1 才为 1,其他情况均为 0。原理如下:
1 & 0 = 0
0 & 0 = 0
1 & 1 = 1
0 & 1 = 0
实例:-5 & 4
-5 的二进制形式为: 11111111 11111111 11111111 11111011
4 的二进制形式为: 00000000 00000000 00000000 00000100
——————————————————————————————
逻辑与运算结果: 00000000 00000000 00000000 00000000
最终结果为0。
12.按位或操作(|)
| 位或操作符,对应的二进制位进行或操作,两个都为 0 才为 0,其他情况均为 1。原理如下:
1 | 0 = 1
0 | 0 = 0
1 | 1 = 1
0 | 1 = 1
实例:-5 | 4
-5 的二进制形式为:11111111 11111111 11111111 11111011
4 的二进制形式为:00000000 00000000 00000000 00000100
————————————————————————————
逻辑或运算结果: 11111111 11111111 11111111 11111111
最终结果为 -1。
利用或的原理我们可以把字节转换为整数,-64 & 0xFF = 192,其中 0xFF 表示整数 255。
13.按位异或操作( ^ )
^ 异或操作符,相同位值为 0 ,不相同为 1。原理如下:
1 ^ 1 = 0
1 ^ 0 = 1
0 ^ 1 = 1
0 ^ 0 = 0
实例:-5 ^ 4
-5 的二进制形式为:11111111 11111111 11111111 11111011
4 的二进制形式为:00000000 00000000 00000000 00000100
————————————————————————————
逻辑异或运算结果: 11111111 11111111 11111111 11111111
最终结果为 -1。
其实利用逻辑异或操作有个作用就是可以比较两个数值是否相等,即利用 1^1 = 0,0^0 = 0 的原理,如 5 ^ 5 == 0。
14.总结
通过上面的分析,我们对 Java 的位运算也算有了比较全面的了解,那么我们的程序通过位运算又有什么优势呢?
其实通过位运算确实会比我们直接的程序代码运算会快很多,因为位运算直接运算的是计算机底层的二进制机器操作指令,而我们的程序代码运算最终也是要转成计算机可识别的二进制操作指令才能执行,位运算可以理解为省了中间转换的操作,处理器可以直接操作。事实是我们在某些源码经常能看见如下代码:
public static final int OP_CONNECT = 1 << 3;
public static final int OP_ACCEPT = 1 << 4;
其实原理是一样的,处理器能够直接支持和处理。