在Java编程中,位运算作为一种底层运算方式,直接操作整型数值的二进制位,其运算效率远超普通的加减乘除运算。但很多开发者在实际工作中却很少用到它,究其原因,大多是因为其语法晦涩、可读性较弱------毕竟代码是写给人看的,高效的同时兼顾易懂性,才是工业级开发的核心诉求。但这并不影响位运算成为面试中的高频考点,也是夯实Java基础的关键知识点,今天就带大家全面吃透位运算与移位运算,从原理到实战,层层拆解、通俗易懂。
一、位运算核心原理
位运算的核心是直接对组成整型数值(如byte、int、long等)的二进制位进行操作,无需经过十进制与二进制的转换中间环节,因此运算速度极快。常用的位运算有4种,分别是按位与(&)、按位或(|)、按位取反(~)、按位异或(^),以下用例均以byte类型(8位二进制)为例,结合负数的补码规则(计算机存储负数以补码形式)详细拆解。
1. 按位与运算(&)
运算规则:两个操作数的对应二进制位,同为1则结果为1,否则为0。
关键补充:byte类型的范围是-128~127,负数的补码计算方式为"反码+1"(反码是对原码按位取反,符号位不变)。例如-10的原码为10001010,反码为11110101,补码为11110110(计算机中实际存储的是补码);8的原码、反码、补码均为00001000。
例题:-10 & 8
拆解过程:
-10(补码):11110110
8(补码): 00001000
按位与结果:00000000(对应十进制0)
2. 按位或运算(|)
运算规则:两个操作数的对应二进制位,同为0则结果为0,其余情况均为1(即只要有一个为1,结果就为1)。
例题:-10 | 8
拆解过程:
-10(补码):11110110
8(补码): 00001000
按位或结果:11111110(对应十进制-2)
3. 按位取反运算(~)
运算规则:对单个操作数的每一位二进制位按位取反,0变1,1变0。
注意点:按位取反是一元运算(只需一个操作数),且取反后结果会遵循补码规则,对于byte类型,取反后会涉及符号位的变化,最终结果与原数满足"~x = -x - 1"的规律。
例题:~8
拆解过程:
8(补码):00001000
按位取反:11110111(对应十进制-9,验证:~8 = -8 -1 = -9)
再如:~(-10)
-10(补码):11110110
按位取反:00001001(对应十进制9,验证:~(-10) = 10 -1 = 9)
4. 按位异或运算(^)
运算规则:两个操作数的对应二进制位,相同则结果为0,不同则结果为1(可记忆为"相同为0,不同为1")。
补充说明:异或运算有两个常用特性------① 一个数异或自身结果为0(x^x = 0);② 一个数异或0结果仍为自身(x^0 = x),这两个特性是异或实现数值交换的核心。
例题:-10 ^ 8(修正原例题结果疏漏,结合补码重新计算)
拆解过程:
-10(补码):11110110
8(补码): 00001000
按位异或结果:11111110(对应十进制-2,原例题结果正确,补充计算逻辑)
二、位运算的实战用途(高频场景+面试考点)
前文提到,位运算的运算效率远高于普通的加减乘除(直接操作二进制位,无需CPU进行复杂的算术运算),但实际工作中使用频率较低,核心原因是其语法晦涩、可读性弱------代码不仅是写给自己看的,更要让同事能快速理解,因此在对效率要求不极致的场景下,开发者更倾向于使用可读性更强的算术运算。
但在以下场景中,若对运算效率要求较高(如底层开发、高频运算场景),位运算就能发挥优势,同时这些场景也是面试中的高频考点,建议重点掌握:
1. 判断int型变量a是奇数还是偶数
核心逻辑:奇数的二进制最低位一定是1,偶数的二进制最低位一定是0;与1(二进制最低位为1,其余为0)进行按位与运算,可快速判断最低位状态。
实现代码:
a & 1 == 0 → 偶数
a & 1 == 1 → 奇数
优势:比a % 2判断更高效(无需进行除法运算)。
2. 求两个int类型变量x、y的平均值(避免溢出)
常规思路:(x + y) / 2,但当x和y均为较大的正数时,x + y的结果可能超过int的最大范围(-2³¹~2³¹-1),导致溢出,最终结果错误。
位运算实现(无溢出):(x & y) + ((x ^ y) >> 1)
逻辑拆解:x & y 获取x和y二进制位中同为1的部分(即两者的公共部分,相当于平均值的整数基础);x ^ y 获取x和y二进制位中不同的部分(相当于两者的差值部分),右移1位等价于除以2,最终将两部分相加得到平均值。
3. 判断一个大于0的整数x是不是2的幂次方
核心逻辑:2的幂次方的二进制特点是"只有一位为1,其余全为0"(如2→10、4→100、8→1000);x & (x - 1) 会将x二进制中最低位的1变为0,若x是2的幂次方,执行后结果为0。
实现代码:((x & (x - 1)) == 0) && (x != 0)
注意:x != 0 是因为0满足(x & (x - 1)) == 0,但0不是2的幂次方。
4. 交换两个int类型变量x、y(无需临时变量,高效)
常规思路:需要定义临时变量temp,temp = x; x = y; y = temp;,会占用额外的内存空间。
位运算实现(无临时变量,性能最优):
x ^= y; // x = x ^ y(此时x存储了x和y的差值特征)
y ^= x; // y = y ^ (x ^ y) = x(利用异或特性x^x=0、0^x=x,将x的值赋给y)
x ^= y; // x = (x ^ y) ^ x = y(将y原来的值赋给x)
注意:该方法仅适用于数值类型,且x和y不能指向同一个内存地址(否则会导致两者都变为0)。
5. 求int类型变量x的绝对值
核心逻辑:int类型是32位,最高位为符号位(0表示正数,1表示负数);x >> 31 会将符号位扩展到整个32位(正数右移31位为0,负数右移31位为-1,即二进制全1)。
实现代码:
int abs(int x) {
int y = x >> 31; // 获取符号位(正数y=0,负数y=-1)
return (x ^ y) - y; // 等价于(x + y) ^ y,负数取反+1,正数不变
}
逻辑验证:x为正数时,y=0,(x^0)-0 = x;x为负数时,y=-1(二进制全1),x^y 等价于按位取反,再减y(减-1即加1),最终得到绝对值。
6. 取模运算(a % (2ⁿ) 等价于 a & (2ⁿ - 1))
核心逻辑:2ⁿ 的二进制是"1后面跟n个0"(如2³=8→1000),2ⁿ - 1 的二进制是"n个1"(如2³-1=7→0111);a & (2ⁿ -1) 会保留a二进制的低n位,等价于a除以2ⁿ的余数。
示例:a=10(1010),n=2(2²=4),10 % 4 = 2;10 & (4-1) = 10 & 3 = 2(1010 & 0011 = 0010)。
注意:该方法仅适用于除数是2的幂次方的场景,且效率远高于常规取模运算。
7. 乘法运算(a * (2ⁿ) 等价于 a << n)
核心逻辑:左移n位,等价于将二进制数整体向左移动n位,右边补0,数值扩大为原来的2ⁿ倍,即a乘以2ⁿ。
示例:a=3(0011),n=2,3 * 4 = 12;3 << 2 = 12(0011 << 2 → 1100)。
8. 除法运算(a / (2ⁿ) 等价于 a >> n)
核心逻辑:右移n位,等价于将二进制数整体向右移动n位,左边补符号位(正数补0,负数补1),数值缩小为原来的1/2ⁿ,即a除以2ⁿ(向下取整)。
示例:a=12(1100),n=2,12 / 4 = 3;12 >> 2 = 3(1100 >> 2 → 0011)。
9. 求一个整数x的相反数(~x + 1)
核心逻辑:根据补码规则,负数的补码是原码取反加1,而一个数的相反数的补码,就是该数补码的取反加1,因此~x + 1 等价于 -x。
示例:x=10(00001010),~10 + 1 = 11110101 + 1 = 11110110(对应十进制-10)。
10. 补充:a % 2 等价于 a & 1
本质是第6点取模运算的特例(n=1,2¹=2,2¹-1=1),也是判断奇偶性的底层逻辑,效率高于常规取模。
三、移位运算(与位运算相辅相成)
移位运算也是操作二进制位的重要运算,分为左移(<<)、有符号右移(>>)、无符号右移(>>>)三种,主要用于实现数值的快速乘除(如前文位运算的7、8点),以下仍以byte类型为例讲解。
1. 左移运算(<<)
运算规则:将操作数的二进制位整体向左移动n位,右边空出来的位用0填补,高位左移后溢出的部分直接舍弃。
核心结论:左移n位,等价于该数乘以2的n次幂(a << n = a * 2ⁿ),效率远高于乘法运算。
示例:3 << 2 → 3 * 2² = 12(0011 << 2 → 1100);8 << 1 → 16(00001000 << 1 → 00010000)。
2. 有符号右移运算(>>)
运算规则:将操作数的二进制位整体向右移动n位,左边空出来的位根据符号位填补(正数符号位为0,补0;负数符号位为1,补1),低位右移后溢出的部分舍弃。
核心结论:有符号右移n位,等价于该数除以2的n次幂(a >> n = a / 2ⁿ),向下取整,效率高于除法运算。
示例:12 >> 2 → 12 / 4 = 3(1100 >> 2 → 0011);-10 >> 1 → -5(11110110 >> 1 → 11111011,对应十进制-5)。
3. 无符号右移运算(>>>)
运算规则:将操作数的二进制位整体向右移动n位,无论原操作数的符号位是0还是1,左边空出来的位一律用0填补,低位右移后溢出的部分舍弃。
关键注意点:
① Java中整型默认是int类型(32位),因此进行无符号右移时,会将操作数先转换为32位二进制数再进行移位操作;
② 无符号右移会将负数的符号位(1)也当作普通位处理,填补0后,负数会变成正数(因为最高位变为0)。
示例:-1(int类型,32位全1)>>> 1 → 2147483647(31位全1,最高位为0,对应int类型的最大值)。
四、面试重点实战题(必掌握)
题目:用最快的速度计算出2 * 16的值(高频面试题,考察移位运算的应用)
核心思路:16是2的4次幂(2⁴=16),根据左移运算的特性,2 * 16 = 2 * 2⁴ = 2 << 4,移位运算效率远高于直接乘法。
验证代码(测试运行时间,单位:纳秒):
public static void main(String[] args) {
方法1:直接乘法运算
long startTime = System.nanoTime(); // 获取开始时间
System.out.println(2 * 16); // 输出结果:32
long endTime = System.nanoTime(); // 获取结束时间
System.out.println("乘法运算运行时间: " + (endTime - startTime) + "ns");
方法2:左移运算(最快)
long startTime1 = System.nanoTime(); // 获取开始时间
System.out.println(2 << 4); // 输出结果:32,2左移4位等价于2*16
long endTime1 = System.nanoTime(); // 获取结束时间
System.out.println("左移运算运行时间: " + (endTime1 - startTime1) + "ns");
}
运行结果说明:多次测试后会发现,左移运算的运行时间远短于乘法运算,这就是移位运算(位运算延伸)的效率优势,也是面试中考察的核心点。
五、总结
位运算与移位运算的核心优势是运算效率极高,本质是直接操作二进制位,跳过了十进制与二进制的转换环节,适合底层开发、高频运算等对效率要求极致的场景。但由于其语法晦涩、可读性较弱,实际业务开发中使用频率较低------毕竟代码的可维护性、可读性,往往比极致的效率更重要(除非有明确的性能瓶颈)。
但对于Java学习者和面试者而言,位运算及其应用场景是必须掌握的知识点:一方面,它能帮助我们更深入理解计算机存储数据的底层逻辑(二进制、补码);另一方面,面试中频繁考察的奇偶判断、数值交换、求平均值(无溢出)等场景,都需要用到位运算的核心思路。