Java Bit Operation Intro


Java 位运算

日常开发中我们使用位运算用的很少,主要是其可读性较差。

但是在计算密集型操作,比如一致性哈希计算、hashmap 扩容、取数据的交集、差集、并集、权限开关位、位操作、移位运算时,位操作的应用就非常广泛了。

1、基础概念

先了解几个基础的概念:机器数、真值、原码、反码、补码。

(1)机器数

无论是代码还是数值,在计算机中都是以二进制的形式存储,而一个数值在计算机中的二进制表示形式就是这个数的机器数。

机器数是有符号位的,在计算机中用一个二进制数的最高位存放符号。正数为 0,负数为 1。

例如 5:

  • +5 :计算机字长为 8 位,它的二进制就是 0000 0101
  • -5:计算机字长为 8 位,它的二进制是 1000 0101(原码)

上面两个二进制表示形式就是机器数。

(2)真值

由于机器数的第一位是符号位,所以其形式值就不等于其真值的数值,也就是说 1000 0101 表示的是-5 而不是 133(1000 0101 的十进制是 131,前提是不算最高位为符号位),因此 -5 才是机器数的真值。

(3)原码

原码是一种计算机中对数字的二进制定点表示方法。原码表示法在数值前面增加了一位符号位(即最高位为符号位):正数该位为 0,负数该位为 1,其余位表示数值的大小。

[ + 5 ] = [0000 0101](原码)

[ -5 ]= [1000 0101](原码)

因为第一位是符号位,因此 8 位二进制的取值范围就是[1111 1111,0111 1111]也就是[-127,127]

  • 正数的原码是其本身
  • 负数的原码是其正数的原码符号位变为 1

注意:0

  • 0 的原码是 00000000
  • -0 的原码是 10000000

(4)反码

反码是数值存储的一种,但是由于补码更能有效表现数字在计算机中的形式,所以多数计算机一般都不采用反码表示数,反码的表示方法如下:

  • 正数的反码是其本身
  • 负数的反码是在其原码的基础上,符号位不变,其余各位取反

[+5]= [00000101](原码)= [00000101](反码)

[ - 5]= [10000101](原码)= [11111010](反码)

注意:0

  • 0 的反码是 00000000
  • -0 的反码是 11111111

(5)补码

在计算机系统中,数值一律用补码来表示和存储。原因在于,使用补码,可以将符号位和数值域统一处理;

同时,加法和减法也可以统一处理。补码的表示方法是:

  • 正数的补码就是其本身
  • 负数的补码是在其原码的基础上,符号位不变,其余各位取反,最后 +1 ,(即在反码的基础上 + 1)

[+5]= [00000101](原码)= [00000101](反码)=[00000101](补码)

[ - 5]= [10000101](原码)= [11111010](反码)=[11111011](补码)

注意:0

  • 补码消除了 +0 和 -0 的冗余和歧义,0 的补码只有一个:0000000

(6)小结

计算机中的符号数有三种表示方法,即原码、反码和补码。三种表示方法均有符号位和数值位两部分,符号位都是用 0 表示 “正”,用 1 表示 “负”,而数值位,三种表示方法各不相同。而在计算机系统中,数值一律用补码来表示和存储。

2、位运算

Java 位移操作:(只针对 int 类型的数据有效,java中,一个 int 的长度始终是 32 位,也就是 4 个字节,它操作的都是该整数的二进制数)。也可作用于以下类型,即 byte,short,char,long(它们都是整数形式)。当为这四种类型时,JVM 先把它们转换成 int 型再进行操作。

例如:int 类型占 4 个字节

private int x1 = 0b00000101;		// 5
private int x2 = 0b11111111111111111111111111111011;	// -5

Java 中对位运算的支持有以下几个运算符:

(1)与

  • &:与运算
结果
1 1 1
1 0 0
0 1 1
0 0 0

(2)或

  • |:或运算
结果
1 1 1
1 0 1
0 1 1
0 0 0

(3)非

  • ~:非运算
结果
0 1
1 0

(4)异或

  • ^:异或运算(半加运算:不带进位的二进制加法)
结果
1 1 0
1 0 1
0 1 1
0 0 0

异或有一些性质,在一些场景下使用它们比较高效:

  • 交换律:任意交换因子位置,结果不变
  • 结合律:(a ^ b)^ c = a ^ (b ^ c)
  • 对任何数 x,都有 x ^ x = 0,x ^ 0 = x,同自己求异或为 0,同 0 求异或为自己:可以用来比较两个数值是否相等
  • 自反性:A ^ B ^ B = A ^ 0 = A,连续和同一个因子做异或运算,最终结果为自己

(5)左移

  • <<:左移

m << n 含义:把整数 m 表示的二进制数左移 n 位,高位移出舍弃 n 位,低位补 0(会出现整数变成负数的可能)。

要注意的是 n < 32,Java 中 int 占据 4 字节 32 位,最高位表示符号,其余位表示数值:

十进制 二进制 结果
1 0000 0000 0000 0000 0000 0000 0000 0001 1
1 << 31 1000 0000 0000 0000 0000 0000 0000 0000 -2147483648
1 << 32 0000 0000 0000 0000 0000 0000 0000 0001 1

为什么 1 左移 31 后变为负数了,因为此时发生了溢出。

在 Java 中 int 型的范围是 [-2147483648, 2147483647]

十进制 二进制补码
2^31 - 1 = 2147483647 0111 1111 1111 1111 1111 1111 1111 1111
0 0000 0000 0000 0000 0000 0000 0000 0000
- (2^31 - 1)= - 2147483647 1000 0000 0000 0000 0000 0000 0000 0001
- 2147483648 1000 0000 0000 0000 0000 0000 0000 0000

由于 Java int 型占 4 个字节 32位,最高位表示符号,所以最大只能表示 0111 1111 1111 1111 1111 1111 1111 1111 即 2^31 - 1。

正常而言,Java 中 int 能表示的最小值应该是 -(2^31 - 1),即 1000 0000 0000 0000 0000 0000 0000 0001

但是如果此时让 2^31 -1 + 1,就会变成 1000 0000 0000 0000 0000 0000 0000 0000,最高位为 1,表示负数,本来这个结果表示原码的 -0,但是我们现在计算机存储二进制用的是补码,在补码中消除了 +0 和 -0 的歧义和冗余,0 只有一个表达式 00000000 00000000 00000000 00000000

所以(Java 环境下)计算机遇到了 1000 0000 0000 0000 0000 0000 0000 0000 的情况时,就有些不知所措,并不知道发生了 Java 整形溢出,如果按照正常输出,这个数值应该是补码的 0,但是 Java 对此做了处理,利用这个特殊的补码扩展了 Java 整形的下限,将 int 的最小值变为 - 2147483648。即:-(2^31 - 1) - 1

注意:Java 中没有无符号左移

小结:m << n 在数字没有溢出的前提下,对于正数和负数,左移 n 位都相当于 m 乘以 2 的 n 次方。

(6)右移

  • >>:右移

m >> n 的含义:把整数 m 表示的二进制数右移 n 位,高位补符号位

  • m 为正数,高位全补 0
  • m 为负数,高位全补 1

小结:

  • m >>n 即相当于 m 除以 2 的 n 次方,得到的结果为整数时,就作为结果。如果结果为小数,此时会出现两种情况:
    • 如果 m 为正数,得到的商会无条件的舍弃小数位;
    • 如果 m 为负数,舍弃小数部分,然后把整数部分 +1 得到位移后的值。

(7)无符号右移

  • >>>:无符号右移

m >>> n:整数 m 表示的二进制数右移 n 位,不论正负数,最高位补 0


Author: NaiveKyo
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint polocy. If reproduced, please indicate source NaiveKyo !
  TOC