Java 位运算基础

在 Java 编程中,位运算是一种直接对二进制位进行操作的运算方式,它在底层操作、性能优化等方面发挥着重要作用。通过理解和运用位运算,我们可以编写更高效、更简洁的代码。

位运算的基本类型

Java 中提供了丰富的位运算符,每种运算符都有其独特的功能和用途。

  • 按位与(&):只有当两个操作数对应位都为 1 时,结果位才为 1,否则为 0。例如,3(二进制 0011)& 5(二进制 0101)的结果为 1(二进制 0001)。在实际应用中,按位与常用于掩码操作,比如提取某个整数的特定位。

  • 按位或(|):只要两个操作数对应位中有一个为 1,结果位就为 1,否则为 0。例如,3(二进制 0011)| 5(二进制 0101)的结果为 7(二进制 0111)。按位或常用于设置标志位,将某些位设置为 1。

  • 按位异或(^):当两个操作数对应位不同时,结果位为 1,否则为 0。例如,3(二进制 0011)^ 5(二进制 0101)的结果为 6(二进制 0110)。按位异或有一个有趣的特性,即一个数与另一个数异或两次会得到原来的数,这在数据加密和解密等场景中有应用。

  • 按位取反(~):将操作数的每一位取反,0 变为 1,1 变为 0。例如,~3(二进制 0011)的结果为 - 4(二进制 1100,这里是补码形式)。按位取反常用于表示负数的补码形式转换。

  • 左移(<<):将操作数的二进制位向左移动指定的位数,右边空出的位用 0 填充。例如,3(二进制 0011)<< 2 的结果为 12(二进制 1100),相当于 3 乘以 2 的 2 次方。左移运算可以快速实现整数乘以 2 的幂次方的操作。

  • 右移(>>):将操作数的二进制位向右移动指定的位数,左边空出的位用符号位(即最高位)填充。对于正数,左边补 0;对于负数,左边补 1。例如,3(二进制 0011)>> 1 的结果为 1(二进制 0001),-3(二进制 1101)>> 1 的结果为 - 2(二进制 1110),相当于 3 除以 2 向下取整,-3 除以 2 向下取整。右移运算可以快速实现整数除以 2 的幂次方的操作。

  • 无符号右移(>>>):将操作数的二进制位向右移动指定的位数,左边空出的位始终用 0 填充,不考虑符号位。例如,-3(二进制 1101)>>> 1 的结果为 2147483646(二进制 01101111111111111111111111111110),这在处理无符号整数时非常有用。

位运算的底层实现原理

在计算机底层,位运算是通过硬件电路实现的。CPU 中的算术逻辑单元(ALU)负责执行各种位运算操作。当程序执行位运算时,编译器会将位运算表达式转换为对应的机器指令,这些指令直接操作 CPU 的寄存器和内存中的二进制数据。由于位运算直接在二进制层面进行操作,避免了复杂的数学运算和数据类型转换,因此具有非常高的执行效率,这也是位运算在性能敏感的场景中被广泛应用的重要原因。

位运算操作符详解

按位与(&)

按位与运算规则是:当两个操作数的对应二进制位都为 1 时,结果位才为 1,否则为 0。例如,5 的二进制表示是 0000 0101,3 的二进制表示是 0000 0011,那么 5 & 3 的计算过程如下:

  0000 0101

& 0000 0011
——————————
  0000 0001

结果为 1。在 Java 代码中,可以这样验证:

int num1 = 5;

int num2 = 3;

int result = num1 & num2;

System.out.println(result);  // 输出1

按位与运算在实际应用中有很多场景。

示例1:权限控制

比如在权限判断中,假设系统定义了不同权限对应不同的二进制位,如查看权限对应第 0 位,编辑权限对应第 1 位,删除权限对应第 2 位等。一个用户的权限用一个整数表示,如果要判断用户是否有查看权限,可以将用户权限值与表示查看权限的二进制数(0000 0001)进行按位与运算,如果结果不为 0,则表示用户有查看权限。示例代码如下:

public class PermissionControl {
    // 定义权限常量
    public static final int READ = 1;      // 0001
    public static final int WRITE = 2;     // 0010
    public static final int EXECUTE = 4;   // 0100
    public static final int DELETE = 8;    // 1000
    
    public static void main(String[] args) {
        // 假设用户拥有 READ 和 WRITE 权限
        int userPermissions = READ | WRITE; // 0011
        
        // 检查用户是否有 READ 权限
        if ((userPermissions & READ) == READ) {
            System.out.println("用户有读取权限");
        }
        
        // 检查用户是否有 DELETE 权限
        if ((userPermissions & DELETE) == DELETE) {
            System.out.println("用户有删除权限");
        } else {
            System.out.println("用户没有删除权限");
        }
    }
}

示例2:奇偶数判断

还可以使用按位与判断整数的奇偶性。奇数的二进制表示中最低位为 1,偶数的最低位为 0。因此,任何整数与 1 进行按位与运算,结果为 1 则是奇数,结果为 0 则是偶数。

public class OddEvenChecker {
    public static void main(String[] args) {
        int num = 25;
        if ((num & 1) == 1) {
            System.out.println(num + " 是奇数");
        } else {
            System.out.println(num + " 是偶数");
        }
    }
}

示例3:子网掩码应用

通过与一个特定的掩码(mask)进行按位与运算,可以屏蔽某些位(将这些位设置为 0),保留其他位的值。计算机网络的子网掩码就是使用该方式进行计算。

public class BitMasking {
    public static void main(String[] args) {
        int num = 0b11111111; // 二进制表示:11111111
        
        // 掩码:保留低4位,屏蔽高4位
        int mask = 0b00001111; // 二进制表示:00001111
        
        int result = num & mask; // 结果:00001111
        
        System.out.println("原始值: " + Integer.toBinaryString(num));
        System.out.println("掩码:   " + Integer.toBinaryString(mask));
        System.out.println("结果:   " + Integer.toBinaryString(result));
    }
}

示例4:提取特定位置的位

按位与也可以用于从一个整数中提取特定位置的位。例如,从一个 32 位整数中提取第 5 到第 8 位。

public class BitExtraction {
    public static void main(String[] args) {
        int num = 0b10101010101010101010101010101010; // 示例值
        
        // 提取第5到第8位(从右往左数,从0开始)
        int position = 4; // 从第5位开始(索引4)
        int length = 4;   // 提取4位
        
        // 生成掩码:00000000000000000000000011110000
        int mask = ((1 << length) - 1) << position;
        
        // 提取指定位
        int extractedBits = (num & mask) >> position;
        
        System.out.println("原始值: " + Integer.toBinaryString(num));
        System.out.println("掩码:   " + Integer.toBinaryString(mask));
        System.out.println("提取的位: " + Integer.toBinaryString(extractedBits));
    }
}

按位或(|)

按位或运算规则是:只要两个操作数的对应二进制位中有一个为 1,结果位就为 1,只有当两个对应位都为 0 时,结果位才为 0。例如,5(0000 0101)| 3(0000 0011)的计算过程如下:

  0000 0101
| 0000 0011
——————————
  0000 0111

结果为 7。在 Java 中:

int num1 = 5;

int num2 = 3;

int result = num1 | num2;

System.out.println(result);  // 输出7

按位或常用于组合多个二进制标志位,每个标志位代表一种权限或状态。例如:

示例1:文件系统权限

假设用 3 个二进制位表示文件的读(R)、写(W)、执行(X)权限:

  • R 权限:二进制 001(十进制 1)

  • W 权限:二进制 010(十进制 2)

  • X 权限:二进制 100(十进制 4)

通过按位或可组合多种权限:

// Java示例
int READ = 1;    // 001
int WRITE = 2;   // 010
int EXECUTE = 4; // 100

// 组合读和写权限
int readWrite = READ | WRITE; // 001 | 010 = 011 (十进制3)

// 组合所有权限
int allPermissions = READ | WRITE | EXECUTE; // 001 | 010 | 100 = 111 (十进制7)

应用场景

  • 文件系统:如 Unix/Linux 的chmod命令使用数字表示权限(如chmod 755)。

  • 用户权限管理:数据库中存储用户角色(管理员、编辑、查看者)的组合权限。

示例2: 配置选项合并

按位或可用于合并多个配置选项,每个选项用一个二进制位表示。

示例:图形渲染设置

假设渲染引擎有以下选项:

  • 抗锯齿:二进制 0001(十进制 1)

  • 阴影:二进制 0010(十进制 2)

  • 纹理过滤:二进制 0100(十进制 4)

  • 动态光照:二进制 1000(十进制 8)

通过按位或合并多个选项:

// Java示例
int ANTIALIASING = 1;    // 0001
int SHADOWS = 2;         // 0010
int TEXTURE_FILTERING = 4; // 0100
int DYNAMIC_LIGHTING = 8;  // 1000

// 启用抗锯齿和阴影
int settings = ANTIALIASING | SHADOWS; // 0001 | 0010 = 0011 (十进制3)

// 启用所有选项
int allSettings = ANTIALIASING | SHADOWS | TEXTURE_FILTERING | DYNAMIC_LIGHTING; 
// 0001 | 0010 | 0100 | 1000 = 1111 (十进制15)

按位异或(^)

按位异或运算规则是:当两个操作数的对应二进制位不同时,结果位为 1,相同时结果位为 0。例如,5(0000 0101)^ 3(0000 0011)的计算过程如下:

  0000 0101
^ 0000 0011
——————————
  0000 0110

结果为 6。Java 代码示例:

int num1 = 5;

int num2 = 3;

int result = num1 ^ num2;

System.out.println(result);  // 输出6

示例1. 交换两个变量的值(无需临时变量)

按位异或可通过三次操作交换两个变量的值,这是其最经典的应用之一:

java

int a = 5;  // 二进制:0101
int b = 3;  // 二进制:0011

// 交换逻辑
a = a ^ b;  // a = 0101 ^ 0011 = 0110 (6)
b = a ^ b;  // b = 0110 ^ 0011 = 0101 (5)
a = a ^ b;  // a = 0110 ^ 0101 = 0011 (3)

System.out.println("a = " + a); // 输出:3
System.out.println("b = " + b); // 输出:5

原理

  • X ^ X = 0(相同值异或为 0)

  • X ^ 0 = X(任何值与 0 异或保持不变)

  • 结合律:(a ^ b) ^ b = a ^ (b ^ b) = a ^ 0 = a

按位取反(~)

按位取反运算会将操作数的每一个二进制位取反,0 变为 1,1 变为 0。例如,~5(0000 0101)的计算过程为:

~ 0000 0101
——————————
  1111 1010

在 Java 中,整数是以补码形式存储的,所以~5 的结果是 - 6。代码验证如下:

int num = 5;

int result = ~num;

System.out.println(result);  // 输出-6

按位取反在负数表示和状态切换等方面有应用。在负数表示中,通过对正数按位取反再加 1 得到其对应的负数补码。在状态切换中,假设一个状态用一个二进制位表示,0 表示关闭,1 表示开启,通过按位取反可以实现状态的切换。

示例1:生成位掩码(屏蔽特定位)

按位取反常与其他位运算符(如&|)配合,生成特定的掩码以操作二进制位。

示例2:保留低 8 位,清除高 24 位

unsigned int x = 0x12345678; // 原始值(32位)
unsigned int mask = ~0xFF;   // 生成高24位为1,低8位为0的掩码(0xFFFFFF00)
x = x & ~mask;               // 等价于 x & 0xFF,保留低8位(0x78)

原理:

  • ~0xFF 表示对低 8 位全 1 的数取反,得到高 24 位全 1、低 8 位全 0 的掩码。

  • 通过 & 运算清除高 24 位,仅保留低 8 位。

左移(<<)和右移(>>)

左移运算(<<)是将操作数的二进制位向左移动指定的位数,右边空出的位用 0 填充。例如,3(0000 0011)左移 2 位的计算过程如下:

0000 0011 << 2
——————————
0000 1100

结果为 12,相当于 3 乘以 2 的 2 次方。在 Java 中:

int num = 3;

int result = num << 2;

System.out.println(result);  // 输出12

右移运算(>>)是将操作数的二进制位向右移动指定的位数,左边空出的位用符号位(即最高位)填充。对于正数,左边补 0;对于负数,左边补 1。例如,3(0000 0011)右移 1 位的计算过程如下:

0000 0011 >> 1
——————————
0000 0001

结果为 1,相当于 3 除以 2 向下取整。而 - 3(1111 1101)右移 1 位的计算过程为:

1111 1101 >> 1
——————————
1111 1110

结果为 - 2,相当于 - 3 除以 2 向下取整。Java 代码示例:

int positiveNum = 3;

int negativeNum = -3;

System.out.println(positiveNum >> 1);  // 输出1

System.out.println(negativeNum >> 1);  // 输出-2

左移和右移常用于快速计算乘以或除以 2 的幂次方的场景,因为位运算的效率比乘法和除法运算更高。

无符号右移(>>>)

无符号右移运算(>>>)是将操作数的二进制位向右移动指定的位数,左边空出的位始终用 0 填充,不考虑符号位。例如,-3(1111 1101)无符号右移 1 位的计算过程如下:

1111 1101 >>> 1
——————————
0111 1110

结果为 2147483646。Java 代码示例:

int num = -3;

int result = num >>> 1;

System.out.println(result);  // 输出2147483646

无符号右移主要用于处理无符号整数,确保在右移时高位始终补 0,避免因符号位扩展带来的问题 。

位运算在 JDK 中的应用实例

ReentrantReadWriteLock 中的位运算

ReentrantReadWriteLock 是 Java 并发包中用于实现读写锁的类。它允许多个线程同时进行读操作,但只允许一个线程进行写操作,在读多写少的场景中能显著提高并发性能。

ReentrantReadWriteLock 内部使用一个 int 类型的变量来维护读锁和写锁的状态,通过位运算来巧妙地区分和管理这两种锁。具体来说,这个 int 变量的高 16 位用于表示读锁的持有数量,低 16 位用于表示写锁的持有数量。例如,假设当前锁状态为 state,那么通过位运算可以获取读锁数量和写锁数量:

// 获取写锁数量
int writeCount = state & 0xFFFF;

// 获取读锁数量
int readCount = state >>> 16;

在获取写锁时,会通过 CAS(Compare and Swap)操作尝试将 state 的低 16 位增加 1,如果成功则表示获取写锁成功。在释放写锁时,同样通过 CAS 操作将低 16 位减 1。

获取读锁时,会先检查写锁是否被持有,如果没有被持有,则通过 CAS 操作将 state 的高 16 位增加 1 来获取读锁。释放读锁时,将高 16 位减 1。

这种通过位运算将读锁和写锁状态存储在同一个变量中的方式,不仅节省了内存空间,还利用了 CAS 操作的原子性,高效地实现了读写锁的并发控制 。

线程池中的位运算

线程池是 Java 并发编程中的重要工具,它通过管理一组线程来复用线程资源,提高任务执行效率。在 Java 中,线程池的核心实现类是 ThreadPoolExecutor。

ThreadPoolExecutor 使用一个 int 类型的变量 ctl 来同时表示线程池的运行状态和线程数量,这其中就运用了位运算。ctl 的高 3 位用于表示线程池的运行状态,低 29 位用于表示线程数量。例如,假设 int 类型是 32 位,那么相关定义如下:

// 线程个数的表示位数
private static final int COUNT_BITS = Integer.SIZE - 3;

// 线程最大数量
private static final int CAPACITY   = (1 << COUNT_BITS) - 1;

// 线程池运行状态:RUNNING
private static final int RUNNING    = -1 << COUNT_BITS;

// 线程池运行状态:SHUTDOWN
private static final int SHUTDOWN   = 0 << COUNT_BITS;


// 线程池运行状态:STOP
private static final int STOP       = 1 << COUNT_BITS;


// 线程池运行状态:TIDYING
private static final int TIDYING    = 2 << COUNT_BITS;


// 线程池运行状态:TERMINATED
private static final int TERMINATED = 3 << COUNT_BITS;

通过位运算来获取线程池的运行状态和线程数量,代码如下:

// 获取线程池状态
private static int runStateOf(int c)     { return c & ~CAPACITY; }


// 获取线程池有效线程数量
private static int workerCountOf(int c)  { return c & CAPACITY; }

在任务提交、线程创建和销毁等操作过程中,都会频繁地对 ctl 进行位运算操作。比如在提交任务时,会根据线程池的状态和线程数量来决定是创建新线程执行任务,还是将任务放入队列,或者直接拒绝任务。这种利用位运算来管理线程池状态和线程数量的方式,使得线程池的操作更加高效和紧凑,充分发挥了位运算在底层控制和性能优化方面的优势 。

位运算的优势与注意事项

位运算的优势

  • 性能高效:位运算直接在二进制层面操作,与复杂的数学运算相比,位运算通常能在单个 CPU 时钟周期内完成,因为它们直接映射到硬件指令。例如,在计算一个数乘以 2 的幂次方时,使用左移运算(如a << n)比使用乘法运算(如a * (1 << n))要快得多。在需要频繁进行计算的场景中,如数据处理、算法实现等,位运算的高效性能够显著提高程序的执行速度。

  • 节省内存:通过位运算可以在一个整数中使用位掩码(bitmask)来表示多个布尔值,从而节省内存空间。在权限管理系统中,可以用一个整数的不同位来表示不同的权限,如读取权限、写入权限、执行权限等。通过位运算来检查和修改这些权限位,相比于使用多个布尔变量,大大节省了内存 。

  • 实现特殊功能:位运算可以实现一些常规运算难以实现的功能。比如通过按位异或运算可以在不使用临时变量的情况下交换两个整数的值;利用按位与运算可以判断一个数是否为 2 的幂次方,只需判断(number & (number - 1)) == 0 && number > 0即可。在一些算法和数据结构的实现中,这些特殊功能可以简化代码逻辑,提高算法效率 。

位运算的注意事项

  • 数据类型限制:位运算主要适用于整型数据,包括byteshortintlong等。对于非整型数据,如floatdouble等,不能直接进行位运算。因为这些数据类型在计算机中的存储方式与整型不同,直接进行位运算会导致编译错误或不可预期的结果。

  • 运算符优先级:位运算符的优先级相对较低,在与其他运算符混合使用时,需要特别注意运算顺序。如果不确定优先级,最好使用括号来明确运算顺序,避免出现逻辑错误。例如,a & b + c(a & b) + c的结果可能截然不同,因为加法运算的优先级高于按位与运算 。

  • 移位操作的边界情况:在进行移位操作时,移位的位数应该是非负整数,并且通常要小于操作数的数据类型的位数。如果移位位数过大,可能会导致不可预测的结果。左移操作可能会导致整数溢出,当移位后的结果超出了数据类型的表示范围时,就会发生溢出,得到错误的结果。

  • 代码可读性:虽然位运算可以提高性能,但过度使用位运算会使代码变得晦涩难懂,降低代码的可读性和可维护性。在编写代码时,应该在性能和代码可读性之间找到平衡。对于一些复杂的位运算操作,最好添加注释来解释其功能和逻辑,以便其他开发者(或未来的自己)能够理解代码的意图。

文章作者: Z
本文链接:
版权声明: 本站所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 微博客
基础
喜欢就支持一下吧