# 基础概念

我们先来了解一下相关的基础概念:机器码真值原码反码补码

# 1️⃣ 机器码

  • 我们知道无论是代码还是数值,在计算机中最后都转换成了二进制的形式存在,而一个数值在计算机中的二进制表示形式,就是这个数的机器码。
  • 机器码是有符号位的,在计算机中用一个二进制数的最高位存放符号,正数为 0 ,负数为 1
    • 示例如下 (使用原码表示):
    • 十进制的 +5 ,计算机字长为 8 位,其二进制是 00000101
    • 十进制的 -5 ,计算机字长为 8 位,其二进制是 10000101
    • 其中的 0000010110000101 就是机器码。

# 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 表示为 负数 ,而数值位三种表示方法各不相同,而在计算机系统中,数值一律使用补码来表示和存储。

# 位运算

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

注意:在 Java 中没有 <<< 符号

符号描述运算规则
&两个位都为 1 时,结果才为 1
|两个位都为 0 时,结果才为 0
^异或两个位相同为 0,不同为 1
~取反所有位置 0 变 1,1 变 0
<<左移各二进位全部左移若干位,高位丢弃,低位补 0
>>带符号右移各二进位全部右移若干位,低位丢弃,高位补为符号位
>>>无符号右移各二进位全部右移若干位,低位丢弃,高位补 0
&&逻辑与左右表达式均为 true 时,运算最终结果才为 true
||逻辑非左右表达式只要有一个为 true,运算最终结果就为 true
BinaryUtils.java
package top.rem.rain;
/**
 * @Author: LightRain
 * @Description: 打印 Java 中 int 整型的在底层的 32 位信息
 * @DateTime: 2023-12-14 22:49
 * @Version:1.0
 **/
public class BinaryUtils {
    public static void print(int num) {
        for (int i = 31; i >= 0; i--) {
            System.out.print((num & (1 << i)) == 0 ? "0" : "1");
        }
        System.out.println();
    }
}

# 1️⃣ 左移 (<<)

  • m << n 的含义是把整数 m 表示的二进制数左移 n 位,高位移出 n 位都舍弃,低位补 0 (此时将会出现正数变成负数的可能),示例如下:
    • 5 << 2 :把十进制的数值 5 左移两位,按照如下步骤计算。
      • 5 转为 16 位的二进制机器码: 00000000 00000000 00000000 00000101
      • 按左移原理,将二进制数左移两位: 00000000 00000000 00000000 00010100
      • 左移后的结果为 20
      Test.java
      public class Test {
        public static void main(String[] args) {
          print(5);
          print(5 << 2);
          print(20);
              /*
               执行结果:
                  00000000000000000000000000000101
                  00000000000000000000000000010100
                  00000000000000000000000000010100
                  整体左移 2 位,高位丢弃,低位补 0
               */
        }
      }
    • 5 << 29 :把十进制的数值 5 左移 29 位,按照如下步骤计算。
      • 5 转为 16 位的二进制机器码: 00000000 00000000 00000000 0000101
      • 按左移原理,将二进制数左移 29 位: 10100000 00000000 00000000 00000000
      • 左移后高位是 1 ,结果显然是负数
  • 总结: m << n 即在数字没有溢出的前提下,对于正数和负数,左移 n 位都相当于 m 乘以 2n 次方。

# 2️⃣ 右移 (>>)

  • m >> n 的含义是把整数 m 表示的二进制数右移 n 位, m 为正数,高位全部补 0m 为负数,高位全部补 1 ,示例如下:
    • 5 >> 2 :把十进制的数值 5 右移两位,按照如下步骤计算
      • 5 转为 16 位的二进制机器码: 00000000 00000000 00000000 00000101
      • 按右移原理,将二进制数左移两位: 00000000 00000000 00000000 00000001
      • 右移后的结果为 1
      Test.java
      public class Test {
        public static void main(String[] args) {
          print(5 >> 2);
          // 将二进制转为十进制  
          int res = Integer.parseInt("00000000000000000000000000000001", 2);
          System.out.println("结果:" + res);
              /*
               执行结果:
                  00000000000000000000000000000001
                  结果:1
               */
        }
      }
    • -5 >> 2 :把十进制的数值 -5 右移两位,按照如下步骤计算
      • -5 转为 16 位的二进制机器码: 11111111 11111111 11111111 11111011
      • 按右移原理,将二进制数右移两位: 11111111 1111111 11111111 11111110
      • 右移后结果为 -2
      Test.java
      public class Test {
        public static void main(String[] args) {
          print(-5);
          print(-5 >> 2);
          print(-2);
              /*
               执行结果:
                    11111111111111111111111111111011
                    11111111111111111111111111111110
                    11111111111111111111111111111110
               */
            
        }
      }
  • 总结: m >> n 即相当于 m 除以 2n 次方,得到的为整数时,即为结果,如果结果为小数,此时会出现两种情况:
    • 如果 m 为正数,得到的商会无条件的舍弃小数位。
    • 如果 m 为负数,舍弃小数部分然后把整数部分 +1 得到位移后的值。

# 3️⃣ 无符号右移 (>>>)

  • m >>> n :整数 m 表示的二进制右移 n 位,不论正负数,高位都补 0 ,示例如下:
    • 5 >>> 2 :把十进制的数值 5 右移两位,按照如下步骤计算
      • 5 转为 16 位的二进制机器码: 00000000 00000000 00000000 00000101
      • 按右移原理,将二进制数左移两位: 00000000 00000000 00000000 00000001
      • 右移后结果为 1
      Test.java
      public class Test {
        public static void main(String[] args) {
          print(5);
          print(5 >>> 2);
          print(1);
              /*
               执行结果:
                  00000000000000000000000000000101
                  00000000000000000000000000000001
                  00000000000000000000000000000001
               */
        }
      }
    • -5 >>> 2 :把十进制的数值 -5 右移两位,按照如下步骤计算
      • -5 转为 16 位的二进制机器码: 11111111 11111111 11111111 11111011
      • 按右移原理,将二进制数右移两位: 00111111 11111111 11111111 11111110
      • 右移后结果为正数: 1073741822
      Test.java
      public class Test {
        public static void main(String[] args) {
          print(-5);
          print(-5 >>> 2);
          // 将二进制转为十进制
          int res = Integer.parseInt("00111111111111111111111111111110", 2);
          System.out.println("结果:" + res);
              /*
               执行结果:
                  11111111111111111111111111111011
                  00111111111111111111111111111110
                  结果:1073741822
               */
        }
      }

# 4️⃣ 按位非操作 (~)

  • ~ 按位取反操作符,对每个二进制位的内容求反,即: 0 变为 11 变为 0 ,示例如下:
    • -5 转为 16 位的二进制机器码: 11111111 11111111 11111111 11111011
    • ~(-5) 取反结果: 00000000 00000000 00000000 00000100
    • 转为十进制结果为 4
    Test.java
    public class Test{
      public static void main(String[] args) {
        print(-5);
        print(~(-5));
        // 将二进制转为十进制
        int res = Integer.parseInt("00000000000000000000000000000100", 2);
        System.out.println("结果:" + res);
            /*
             执行结果:
                11111111111111111111111111111011
                00000000000000000000000000000100
                结果:4
             */
      }
    }

# 5️⃣ 按位与操作 (&)

  • & 位与操作,对应的二进制位进行进行与操作,两个都为 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
    Test.java
    public class Test{
      public static void main(String[] args) {
        print(-5);
        print(4);
        print(-5 & 4);
            /*
             执行结果:
                11111111111111111111111111111011
                00000000000000000000000000000100
                00000000000000000000000000000000
             */
      }
    }

# 6️⃣ 按位或操作 (|)

  • | 位或操作符,对应的二进制位进行或操作,两个都为 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
    Test.java
    public class Test{
      public static void main(String[] args) {
        print(-5);
        print(4);
        print(-5 | 4);
            /*
             执行结果:
                11111111111111111111111111111011
                00000000000000000000000000000100
                11111111111111111111111111111111
             */
      }
    }
  • 利用或的原理我们可以把字节转换为整数, -64&0xFF = 192 其中都 0xFF 表示整数 255
    Test.java
    public class Test{
      public static void main(String[] args) {
        System.out.println(-64 & 0xFF);
        System.out.println(0xFF);
            /*
             执行结果:
                192
                255
             */
      }
    }

# 7️⃣ 按位异或操作 (^)

  • ^ 异或操作符,相同位值为 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
    Test.java
    public class Test{
      public static void main(String[] args) {
        print(-5);
        print(4);
        print(-5 ^ 4);
        print(-1);
            /*
             执行结果:
                11111111111111111111111111111011
                00000000000000000000000000000100
                11111111111111111111111111111111
                11111111111111111111111111111111
             */
      }
    }
  • 利用逻辑异或操作有个作用就是可以比较两个数值是否相等,即:利用 1 ^ 1 = 00 ^ 0 = 0 的原理,如: 5 ^ 5 == 0
    Test.java
    public class Test{
      public static void main(String[] args) {
        System.out.println(1 ^ 1);
        System.out.println(0 ^ 0);
        System.out.println(5 ^ 5);
            /*
             执行结果:
                0
                0
                0
             */
      }
    }

# 8️⃣ 总结

有了上面的分析,我们对 Java 的位运算有了比较全面的了解,那么我们的程序通过位运算又有什么优势呢?其实通过位运算确实会比我们直接的代码程序运算会快很多,因为位运算直接运算的是计算机底层的二进制机器码操作指令,而我们的代码程序运算最终也是要转成计算机可以识别的二进制操作指令才能完成,位运算可以理解为省去了中间转换的操作,处理器可以直接操作。我们会在某些源码中经常能看到位运算的代码,其实原理是一样的,处理器能够直接支持和处理。

static final int SHARED_SHIFT   = 16;
static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;