学过 java 或其他编程语言的都听过,位运算 这个名词。实际开发中很少会使用到,但又听说是基础又重要。
- 在计算机中所有数据都是以二进制的形式储存的。位运算其实就是直接对在内存中的二进制数据进行操作,因此处理数据的速度非常快。
那么,位运算真的就比加减乘除运算快吗?试试便知
public class BitTimeTest {
public static long doOp() {
long start = System.currentTimeMillis();
System.out.println(start);
int n = 0;
for (int i = 1; i <= 100000; i++) {
for (int j = 1; j <= 10000; j++) {
/* 1. 相当于常数计算赋值,速度非常快 */
// n = j * 16;
/* 2. 操作n 并赋值给n, 速度明显较慢 */
// n = n * 16;
/* 3. 连续两次操作n 并赋值给n, 速度明显变慢 */
// n = n * 16;
// n = n / 32;
/* 4. 连续三次操作n 并赋值给n, 速度明显变慢 */
n = n * 16;
n = n / 32;
n = n % 16;
}
}
long end = System.currentTimeMillis();
System.out.println(end);
return end - start;
}
public static long doBit() {
long start = System.currentTimeMillis();
System.out.println(start);
int n = 0;
for (int i = 1; i <= 100000; i++) {
for (int j = 1; j <= 10000; j++) {
/* 1. 相当于常数计算赋值,速度非常快 */
// n = j << 4;
/* 2. 操作n 并赋值给n, 速度明显较慢 */
// n = n << 4;
/* 3. 连续两次操作n 并赋值给n, 速度明显变慢 */
// n = n << 4;
// n = n >> 5;
/* 4. 连续三次操作n 并赋值给n, 速度明显变慢 */
n = n << 4;
n = n >> 5;
n = n & 15;
}
}
long end = System.currentTimeMillis();
System.out.println(end);
return end - start;
}
public static void main(String[] args) {
System.out.println( "乘除所用时间: " + doOp());
System.out.println("位运算所用时间: " + doBit());
}
}
- 情况1:两次运行,乘除所用时间与位运算差不多,乘除甚至比位运算略快
- 情况2:与情况1类似,相差无几,乘除也可能更快
- 情况3:多次操作位运算,速度明显比乘除少
- 情况4:将 i、j 的范围缩小为 100000、10000,可以看到,两者差距越来越大
- 总结:位运算确实比加减乘除快,但在简单的条件、执行语句少的情况下,互有胜负。
正文开始(位与&、或|、异或^、非~、移位>>)
所有的运算,都是转换成二进制计算的。
- 此方法为公用方法,指定位数的高位补 0 显示
private static String getFormatStr(int num, int len) {
String integerMaxValueStr = Integer.toBinaryString(num);
int a = len;
StringBuilder sb = new StringBuilder();
int l = integerMaxValueStr.length();
int i = 0;
for (; a > 0; --a) {
if (--l >= 0) {
sb.append(integerMaxValueStr.charAt(l));
} else {
sb.append("0");
}
if (++i % 4 == 0) {
if (a > 1) {
sb.append("-");
}
i = 0;
}
}
return sb.reverse().toString();
}
1. 位与 &
- 位与:按位对其,11为1,其余为0
public class BitAndTest {
public static void main(String[] args) {
int i = 5;
int j = 3;
System.out.println("i = 5 的二进制32位为: " + getFormatStr(i, 32));
System.out.println("j = 3 的二进制32位为: " + getFormatStr(j, 32));
int res = i & j;
System.out.println("--------------------------------------------------------------------");
System.out.println("res = i & j 的二进制32位为: " + getFormatStr(res, 32));
System.out.println("res = " + res);
}
}
- 结果如下
2. 位或|
- 位或:有1为1, 00为0
public class BitOrTest {
public static void main(String[] args) {
int i = 5;
int j = 3;
System.out.println("i = 5 的二进制32位为: " + getFormatStr(i, 32));
System.out.println("j = 3 的二进制32位为: " + getFormatStr(j, 32));
int res = i | j;
System.out.println("--------------------------------------------------------------------");
System.out.println("res = i | j 的二进制32位为: " + getFormatStr(res, 32));
System.out.println("res = " + res);
}
}
3. 异或^
- 异或:不同为1,相同为0
public class BitXORTest {
public static void main(String[] args) {
int i = 5;
int j = 3;
System.out.println("i = 5 的二进制32位为: " + getFormatStr(i, 32));
System.out.println("j = 3 的二进制32位为: " + getFormatStr(j, 32));
int res = i ^ j;
System.out.println("--------------------------------------------------------------------");
System.out.println("res = i ^ j 的二进制32位为: " + getFormatStr(res, 32));
System.out.println("res = " + res);
}
}
4. 非~
- 非:一元操作符,0 变 1,1 变 0
public class BitNegateTest {
public static void main(String[] args) {
int i = 5;
System.out.println("i = 5 的二进制32位为: " + getFormatStr(i, 32));
int res = ~i;
System.out.println("--------------------------------------------------------------------");
System.out.println("res = ~i 的二进制32位为: " + getFormatStr(res, 32));
System.out.println("res = " + res);
}
}
- 一看,结果为负数,问题来了,二进制怎么表示负数呢?请接着往下看。
5. 二进制负数的表示方法
- java 中二进制的负数表示,与 原码、反码、补码紧密相关。
- 注意一点,计算机使用的是补码,且做位运算时,都会补全32位。(short 也是)
5.1 原码
- 原码:将第一位作为符号位,其余位表示值。
int i = 1 原码: 0000-0000-0000-0000-0000-0000-0000-0001
int j = -1 原码: 1000-0000-0000-0000-0000-0000-0000-0001
5.2 反码
反码:正数的反码与原码相同,负数的反码,符号位不变,其余为取反
int i = 1 原码: 0000-0000-0000-0000-0000-0000-0000-0001
int i = 1 反码: 0000-0000-0000-0000-0000-0000-0000-0001
int j = -1 原码: 1000-0000-0000-0000-0000-0000-0000-0001
int j = -1 反码: 1111-1111-1111-1111-1111-1111-1111-1110
5.3 补码
补码:正数的补码和原码、反码一样,负数的补码就是反码+1。
int i = 1 原码: 0000-0000-0000-0000-0000-0000-0000-0001
int i = 1 反码: 0000-0000-0000-0000-0000-0000-0000-0001
int i = 1 补码: 0000-0000-0000-0000-0000-0000-0000-0001
int j = -1 原码: 1000-0000-0000-0000-0000-0000-0000-0001
int j = -1 反码: 1111-1111-1111-1111-1111-1111-1111-1110
int j = -1 补码: 1111-1111-1111-1111-1111-1111-1111-1111
5.4 使用补码进行运算
- 结果出现了33位,但整形数据只认32位,所以结果为 2。
int i = 5 - 3 = 5 + (-3) <==> 2;
/* 验证 */
int 5 的补码: 0000-0000-0000-0000-0000-0000-0000-0101
int -3 的补码: 1111-1111-1111-1111-1111-1111-1111-1101
-------------------------------------------------------
结果 :1-0000-0000-0000-0000-0000-0000-0000-0010
6. 移位 <<、>>、>>>
- 移位分为:左移、右移
- 右移又分为:有符号左移,无符号右移
6.1 左移 <<
- 依据二进制特性,(一定范围内)左移一位相当于 *2 操作。
public class BitLeftMoveTest {
public static void main(String[] args) {
int i = 10;
System.out.println("i = 10 的二进制32位为: " + getFormatStr(i, 32));
int res = i << 30;
System.out.println("-----------------------------------------------------------------------");
System.out.println("res = i << 30 的二进制32位为: " + getFormatStr(res, 32));
System.out.println("res = " + res);
System.out.println("========================================================================");
int j = -10;
System.out.println("i = -10 的二进制32位为: " + getFormatStr(j, 32));
int res2 = j << 30;
System.out.println("-----------------------------------------------------------------------");
System.out.println("res2 = j << 30 的二进制32位为: " + getFormatStr(res, 32));
System.out.println("res2 = " + res2);
}
}
6.2 右移
6.2.1 有符号右移 >>
- 依据二进制特性,正数右移一位相当于 /2 操作,负数右移不确定。
public class BitRightMoveTest {
public static void main(String[] args) {
int i = 20;
System.out.println("i = 20 的二进制32位为: " + getFormatStr(i, 32));
int res = i >> 4;
System.out.println("--------------------------------------------------------------------");
System.out.println("res = i >> 4 的二进制32位为: " + getFormatStr(res, 32));
System.out.println("res = " + res);
System.out.println("=====================================================================");
int j = -20;
int res2 = j >> 4;
System.out.println("j = -20 的二进制32位为: " + getFormatStr(j, 32));
System.out.println("--------------------------------------------------------------------");
System.out.println("res2 = j >> 4 的二进制32位为:" + getFormatStr(res2, 32));
System.out.println("res2 = " + res2);
}
}
- 右移>> 将bit位移动相应的位数,高位补上符号位的值,即正数补0,负数补1
6.2.2 无符号右移 >>>
- 无符号右移>>>:与 >> 的区别是高位都补0
public class BitNonSymbolRightMoveTest {
public static void main(String[] args) {
int i = 20;
System.out.println("i = 20 的二进制32位为: " + getFormatStr(i, 32));
int res = i >>> 4;
System.out.println("--------------------------------------------------------------------");
System.out.println("res = i >>> 4 的二进制32位为: " + getFormatStr(res, 32));
System.out.println("res = " + res);
System.out.println("=====================================================================");
int j = -20;
int res2 = j >>> 4;
System.out.println("j = -20 的二进制32位为: " + getFormatStr(j, 32));
System.out.println("--------------------------------------------------------------------");
System.out.println("res2 = j >>> 4 的二进制32位为:" + getFormatStr(res2, 32));
System.out.println("res2 = " + res2);
}
}
7. 为什么没有无符号左移?
- 左移操作就是将bit移动相应的位数,在低位补0。由于符号位是在最高位的定义,所以只能在低位补0,这是跟右移高位补0或1的区别。
- 具有此问题的,应该是想问,那为什么不保留最高位符号位进行左移?好问题,我也想知道,但是java不这么定义啊,那我们就遵循好了。
8. 计算机数值为什么使用补码表示?
- 当二进制开始代入现实的加减运算时,问题就来了(以byte 8位为例)
8.1、 若使用原码计算
情况1:若计算 1 + 1,结果为 2 没问题
数值1:0000-0001
数值1:0000-0001
-------------------
结果: 0000-0010 ==> 2
情况:若计算 1 - 1 = 1 + (-1), 结果为 -2 ,问题很大
数值1: 0000-0001
数值-1:1000-0001
-------------------
结果: 1000-0010 ==> -2
8.2、 若使用反码计算
- 原码的减法出现问题,那么试试用反码计算
情况1:若计算 1 - 1 = 1 + (-1), 结果为 -0 ,
数值1(反码): 0000-0001
数值-1(反码):1111-1110
-------------------
结果(反码): 1111-1111 ==> 结果(原码): 1000-0000 ==> -0
- 结果的真值部分正确,虽然 +0 和 -0在人们的理解中是一样的,但计算机中使用 1000-0000 和 0000-0000 表示同一个数,显然不是很科学
- 那么,再来试试其他数的减法
情况2:若计算 2 - 1 = 1 + (-1), 结果为 0 ,
数值2(反码): 0000-0010
数值-1(反码):1111-1110
-------------------
结果(反码):1-0000-0000 ==> 舍弃出位,结果(原码): 0000-0000 ==> 0
情况3:若计算 5 - 3 = 5 + (-3), 结果为 1 ,
数值5(反码): 0000-0101
数值-3(反码):1111-1100
-------------------
结果(反码):1-0000-0001 ==> 舍弃出位,结果(原码): 0000-0001 ==> 1
情况4:若计算 7 - 1 = 7 + (-1), 结果为 5 ,
数值7(反码): 0000-0111
数值-1(反码):1111-1110
-----------------------
结果(反码):1-0000-0101 ==> 舍弃出位,结果(原码): 0000-0101 ==> 5
- 从上面貌似看出了一点规律,+1 就能得出正确结果
8.3、若使用补码计算
- 正数原码、反码、补码都一样,负数的补码是反码+1
情况1:若计算 1 - 1 = 1 + (-1), 结果为 -0 ,
数值1(补码): 0000-0001
数值-1(补码):1111-1111
-------------------
结果(补码): 1-0000-0000 ==> 弃位结果(原码): 0000-0000 ==> 0
情况2:若计算 7 - 1 = 7 + (-1), 结果为 6 ,
数值7(补码): 0000-0111
数值-1(补码):1111-1111
-----------------------
结果(补码):1-0000-0110 ==> 舍弃出位,结果(原码): 0000-0110 ==> 6
- 再回看, 补码1000-0000表示 -128,既解决了原码与反码的问题,还表示多了-128的这个数,也可以直接相加减,所以计算机的存储一直是补码形式。
9. 计算机不认识加减符号,那么是怎么进行运算的?
- 请看这篇文章 位运算实现加减乘除