学过 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类似,相差无几,乘除也可能更快

java非运算详解_补码


java非运算详解_bit_02

  • 情况3:多次操作位运算,速度明显比乘除少
  • java非运算详解_java_03


  • java非运算详解_bit_04

  • 情况4:将 i、j 的范围缩小为 100000、10000,可以看到,两者差距越来越大
  • java非运算详解_bit_05


  • java非运算详解_System_06

  • 总结:位运算确实比加减乘除快,但在简单的条件、执行语句少的情况下,互有胜负。

正文开始(位与&、或|、异或^、非~、移位>>)

所有的运算,都是转换成二进制计算的。

  • 此方法为公用方法,指定位数的高位补 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);
    }
}

java非运算详解_java非运算详解_07

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);
    }
}

java非运算详解_java非运算详解_08

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);
    }
}

java非运算详解_java_09

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);
    }
}

java非运算详解_java非运算详解_10

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. 计算机不认识加减符号,那么是怎么进行运算的?

  • 请看这篇文章 位运算实现加减乘除