Java 位运算操作

Java 基本数据类型
序号 数据类型 大小/位
1 long(长整数) 64
2 int(整数) 32
3 short(短整数) 16
4 byte(位) 8
5 char(字符) 2
6 float(单精度) 32
7 double(双精度) 64

 


 在演示位运算前先进行一些科普,计算机中数字是以二进制形式存储,但是计算机存储数字时,不是直接存储该数字对应的二进制数,而是存储该数字对应二进制的补码,位运算也是补码之间的计算。

下面介绍几个概念:原码、反码、补码

在此之前先了解 机器数 和 真值 的概念。

机器数:

 一个数在计算机中的存储形式是二进制数,我们称这些二进制数为机器数,机器数是有符号,在计算机中用机器数的最高位存放符号位,0表示正数,1表示负数。

真值:

因为机器数带有符号位,所以机器数的形式值不等于其真实表示的值(真值),以机器数1000 0001为例,其真正表示的值(首位为符号位)为 -1,而形式值为129(二进制 1000 0001 转为十进制即为 129);因此将带符号的机器数真正表示的值称为机器数的真值。(即 1000 0001 的真值是 -1, 形式值是129)


 1)原码

原码的表示与机器数真值表示的一样,即用第一位表示符号,其余位表示数值。

例如: 

0000 0001 表示 +1

1000 0001 表示 -1

2)反码

正数的反码与原码相同;负数的反码除符号位(最高位)不变,其余取反。

如 -1 的原码:1000 0001,反码则是 1111 1110

3)补码

正数的补码与原码相同;负数的补码为其反码 + 1 。

如 -1 的原码:1000 0001,反码 1111 1110,补码则是 1111 1111  。


 数据在计算机中的存储形式

计算机实际只存储补码,所以原码转换为补码的过程,也可以理解为数据存储到计算机内存中的过程,以8位二进制数为例:

在原、反、补码中,正数的表示是一模一样的,而负数的表示是不相同的,所以对于负数的补码来说,我们是不能直接用进制转换将其转换为十进制数值的,因为这样是得不到计算机真正存储的十进制数的,所以应该将其转换为原码后,再将转换得到的原码进行进制转换为十进制数(机器数包含符号位),即 1111 1111 的真值存储的十进制数是 -1,而不是255。


 

原码、反码、补码演进过程

上面说过,原码、反码、补码的表示对于正数来说都是一样的,而对于负数来说,三种码的表示确是完全不同的,那大家是否会有个疑问:如果原码才是我们人类可以识别并用于直接计算的表示方式,那为什么还会有反码和补码?计算机直接存储原码不就完事了?

在解决这些问题前,我们先来了解计算机的底层概念,我们人脑可以很轻松的知道机器数的第一位是符号位,但对于计算机基础电路设计来说判别第一位是符号位是非常难和复杂的事情,为了让计算机底层设计更加简单,人们开始探索将符号位参与运算,并且采用只保留加法的方法,我们知道减去一个数,等于加上这个数的负数,即:1 - 1 = 1 + (-1) = 0,这样让计算机运算就更加简单了,并且也让符号位参与到运算中去 。

计算 1 - 1 = 0  即 1 + (-1) = 0

1)使用原码计算:

0000 0001 + 1000 0001 = 1000 0010 = -2

结论:如果用原码表示,让符号位也参与计算,对于减法来说,结果是不正确的。这也是计算机内部在存储数据时不使用原码的原因,为了解决这一问题,出现了反码。

2)使用反码计算:

原 0000 0001 + 1000 0001

取反则为 0000 0001 + 1111 1110 = 1111 1111(反码)

反码 1111 1111 = 原码 1000 0000 = -0(真值)

结论:通过计算我们发现用反码计算减法,结果的真值部分是正确的。而唯一的问题出现在"0"这个特殊的数值上,虽然人们理解上 +0 和 -0 是一样的,但是 0 带符号是没有任何意义的,而且会有原码[0000 0000] 和 原码[1000 0000] 两个编码表示0。为了解决这一问题,出现了补码。

3)使用补码计算:

原 0000 0001 + 1000 0001

取补则为 0000 0001 + 1111 1111 = 0000 0000(补码)

补码 0000 0000 = 原码 0000 0000 = 0(真值)(这里你可能会疑惑,如何用补码 0000 0000 倒推到原码 0000 0000的,其实可以以 0 的二进制形式反推,0 的原码 0000 0000,按照最高位是符号位,那么 0 在计算机中认为是正数,而正数的原码、反码、补码都相同,所以 0 的原、反、补码都是 0000 0000) 。

结论:这样 0 用 [0000 0000] 表示,以前出现问题的 -0 则不存在了。而且人们还发现可以用 [1000 0000] 表示 -128,-128 的推算过程如下:

(-1) + (-127) = -128

原 1000 0001 + 1111 1111

取补则为 1111 1111 + 1000 0001 = 1000 0000(补码、高位溢出)

也就是说补码 1000 0000 代表的真值是 -128(站在原码的角度,1000 0000 表示 -0,所以实际上是用 -0 的补码来表示 -128,所以 -128 并没有原码和反码,通过补码 1000 0000 倒推得到 -128 的原码是 0000 0000 这是不正确的!!!  错误的倒推过程:补码 1000 0000  -> 取反得到 0111 1111 -> 取原码得到 0000 0000)。

由此不难推出为何 byte(8位)取值范围为 [-128,127] (因为最大的正数 0111 1111 = 127,那么自然最小的负数是 -127,而补码可多表示一个最低数,即 -128,所以byte的取值范围是 [-128, 127])。

---------------------- 抛开其他不谈,8位二进制数,0000 0000 表示 +0, 1000 0000 表示 -0,两个数据都代表0,岂不浪费?所以用 1000 0000 多表示一个最低数,也充分利用了二进制可存储的数据空间。


 接下来看下二进制的位运算(即补码的位运算)

 

Java 位运算符
序号 位运算符 描述
1 & 按位与
2 | 按位或
3 ^ 异或(相同为0,不同为1)
4 ~ 取反
5 << 左移位
6 >> 右移位
7 >>> 无符号右移位

二进制进行与、或、异或操作的结果如下:

序号 二进制数a 二进制数b 与(&) 或(|) 异或(^)
1 0 0 0 0 0
2 0 1 0 1 1
3 1 0 0 1 1
4 1 1 1 1 0

 

如果一个int型(32位)变量进行位运算时,需将其变为二进制数据,然后进行位运算操作,范例:

public class OperatorDemo {
    public static void main(String[] args) {
        int x = 3;
        int y = 6;
        System.out.println(x & y);
        System.out.println(x | y);
        System.out.println(x ^ y);
    }
}
/*
程序运行结果:
2
7
5
*/

运算过程分析:

int 类型长度为32位,3对应的二进制为 11,所以要在前面补30个0,6对应的二进制数为110,前面补29个0,计算过程如下:

public class OperatorDemo {
    public static void main(String[] args) {
        //范例,-3 取反
        int i = ~-3;
        System.out.println("-3取反结果:" + i);
    }
}
/*
程序运行结果:
-3取反结果:2
*/

运算过程分析:

时刻强调:位运算是以补码运算的!!!

 

public class OperatorDemo {
    public static void main(String[] args) {
        //范例,3左移2位  -3左移2位
        int m = 3 << 2;
        int n = -3 << 2;
        System.out.println("3左移2位结果:" + m);
        System.out.println("-3左移2位结果:" + n);
    }
}
/*
程序运行结果:
3左移2位结果:12
-3左移2位结果:-12
*/

运算过程分析:

左移操作是将运算数的二进制码整体左移指定位数,左移之后的空位使用0来填充。

public class OperatorDemo {
    public static void main(String[] args) {
        //范例,3右移2位  -3右移2位
        int m = 3 >> 2;
        int n = -3 >> 2;
        System.out.println("3右移2位结果:" + m);
        System.out.println("-3右移2位结果:" + n);
    }
}
/*
程序运行结果:
3右移2位结果:0
-3右移2位结果:-1
*/

运算过程分析:

右移操作是将运算数的二进制码整体右移指定位数,右移之后的空位使用符号位来填充,正数填充0,负数填充1。

public class OperatorDemo {
    public static void main(String[] args) {
        //范例,3无符号右移2位  -3无符号右移2位
        int m = 3 >>> 2;
        int n = -3 >>> 2;
        System.out.println("3无符号右移2位结果:" + m);
        System.out.println("-3无符号右移2位结果:" + n);
    }
}
/*
程序运行结果:
3无符号右移2位结果:0
-3无符号右移2位结果:1073741823
*/

运算过程分析:

无符号右移与右移的区别在于,无符号右移以0填充空位,与符号位无关。

 

 

注意:位运算只适用于 long, int, short, byte, char 类型。

 

 

附言:

部分内容摘自:原码、反码、补码

posted @ 2023-09-19 21:40  胡子叔叔  阅读(113)  评论(0编辑  收藏  举报