在平时看各种框架的源码的过程中, 经常会看到一些位移运算, 所以作为一个 Java 开发者是一定掌握位移运算的.
正数位移运算
Java 中有三个位移运算:
<<: 左移
>>: 右移
>>>: 无符号右移
我们直接看一下 Demo:
- System.out.println(2 <<1); // 4
- System.out.println(2>> 1); // 1
- System.out.println(2>>> 1); // 1
- System.out.println(-2 <<1); // -4
- System.out.println(-2>> 1); // -1
- System.out.println(-2>>> 1); // 2147483647
乍一眼看到上面 Demo 的打印结果, 你应该是懵逼的, 接下来我来解释一下这个结果到底是如何运算出来的.
上面的 Demo 中有 "2" 和 "-2", 这是两个十进制数, 并且是 int 类型的 (java 中占四个字节), 位运算是基于二进制 bit 来的, 所以我们需要将十进制转换为二进制之后再进行运算:
2 <<1: 十进制 "2" 转换成二进制为 "00000000 00000000 00000000 00000010", 再将二进制左移一位, 高位丢弃, 低位补 0, 所以结果为 "00000000 00000000 00000000 00000100", 换算成十进制则为 "4"
2>> 1: 十进制 "2" 转换成二进制为 "00000000 00000000 00000000 00000010", 再将二进制右移一位, 低位丢弃, 高位补 0, 所以结果为 "00000000 00000000 00000000 00000001", 换算成十进制则为 "1"
对于这两种情况非常好理解, 那什么是无符号右移, 以及负数是怎么运算的呢?
我们先来看 - 2 <<1 与 - 2>> 1, 这两个负数的左移与右移操作其实和正数类似, 都是先将十进制数转换成二进制数, 再将二进制数进行移动, 所以现在的关键是负数如何用二进制数进行表示.
原码, 反码, 补码
杰西莱我们主要介绍十进制数用二进制表示的不同方法, 所以为了简洁, 我们用一个字节, 也就是 8 个 bit 来表示二进制数.
原码
十进制 | 原码 |
---|---|
2 | 0000 0010 |
-2 | 1000 0010 |
原码其实是最容易理解的, 只不过需要利用二进制中的第一位来表示符号位, 0 表示正数, 1 表示负数, 所以可以看到, 一个数字用二进制原码表示的话, 取值范围是 - 111 1111 ~ +111 1111, 换成十进制就是 - 127 ~ 127.
反码
在数学中我们有加减乘除, 而对于计算机来说最好只有加法, 这样计算机会更加简单高效, 我们知道在数学中 5-3=2, 其实可以转换成 5+(-3)=2, 这就表示减法可以用加法表示, 而乘法是加法的累积, 除法是减法的累积, 所以在计算机中只要有加法就够了.
一个数字用原码表示是容易理解的, 但是需要单独的一个 bit 来表示符号位. 并且在进行加法时, 计算机需要先识别某个二进制原码是正数还是负数, 识别出来之后再进行相应的运算. 这样效率不高, 能不能让计算机在进行运算时不用去管符号位, 也就是说让符号位也参与运算, 这就要用到反码.
十进制 | 原码 | 反码 |
---|---|---|
2 | 0000 0010 | 0000 0010 |
-2 | 1000 0010 | 1111 1101 |
正数的反码和原码一样, 负数的反码就是在原码的基础上符号位保持不变, 其他位取反.
那么我们来看一下, 用反码直接运算会是什么情况, 我们以 5-3 举例.
5 - 3 等于 5 + (-3)
十进制 | 原码 | 反码 |
---|---|---|
5 | 0000 0101 | 0000 0101 |
-3 | 1000 0011 | 1111 1100 |
- 5-3
- = 5+(-3)
- = 0000 0101(反码) + 1111 1100(反码)
- = 0000 0001(反码)
- = 0000 0001(原码)
- = 1
这不对呀?!! 5-3=1?, 为什么差了 1?
我们来看一个特殊的运算:
- 1-1
- = 1+(-1)
- = 0000 0001(反码) + 1111 1110(反码)
- = 1111 1111(反码)
- = 1000 0000(原码)
- = -0
我们来看一个特殊的运算:
- 0+0
- = 0000 0000(反码) + 0000 0000(反码)
- = 0000 0000(反码)
- = 0000 0000(原码)
- = 0
我们可以看到 1000 0000 表示 - 0,0000 0000 表示 0, 虽然 - 0 和 0 是一样的, 但是在用原码和反码表示时是不同的, 我们可以理解为在用一个字节表示数字取值范围时, 这些数字中多了一个 - 0, 所以导致我们在用反码直接运算时符号位可以直接参加运算, 但是结果会不对.
补码
为了解决反码的问题就出现了补码.
十进制 | 原码 | 反码 | 补码 |
---|---|---|---|
2 | 0000 0010 | 0000 0010 | 0000 0010 |
-2 | 1000 0010 | 1111 1101 | 1111 1110 |
正数的补码和原码, 反码一样, 负数的补码就是反码 + 1.
十进制 | 原码 | 反码 | 补码 |
---|---|---|---|
5 | 0000 0101 | 0000 0101 | 0000 0101 |
-3 | 1000 0011 | 1111 1100 | 1111 1101 |
- 5-3
- = 5+(-3)
- = 0000 0101(补码) + 1111 1101(补码)
- = 0000 0010(补码)
- = 0000 0010(原码)
- = 2
5-3=2!! 正确.
再来看特殊的:
- 1-1
- = 1+(-1)
- = 0000 0001(补码) + 1111 1111(补码)
- = 0000 0000(补码)
- = 0000 0000(原码)
- = 0
1-1=0!! 正确
再来看一个特殊的运算:
- 0+0
- = 0000 0000(补码) + 0000 0000(补码)
- = 0000 0000(补码)
- = 0000 0000(原码)
- = 0
0+0=0!! 也正确.
所以, 我们可以看到补码解决了反码的问题.
所以对于数字, 我们可以使用补码的形式来进行二进制表示.
负数位移运算
我们再来看 - 2 <<1 与 - 2>> 1.
-2 用原码表示为 10000000 00000000 00000000 00000010
-2 用反码表示为 11111111 11111111 11111111 11111101
-2 用补码表示为 11111111 11111111 11111111 11111110
-2 <<1, 表示 - 2 的补码左移一位后为 11111111 11111111 11111111 11111100, 该补码对应的反码为
- 11111111 11111111 11111111 11111100
- - 1
- = 11111111 11111111 11111111 11111011
该反码对应的原码为: 符号位不变, 其他位取反, 为 10000000 00000000 00000000 00000100, 表示 - 4.
所以 - 2 << 1 = -4.
同理 - 2>> 1 是一样的计算方法, 这里就不演示了.
无符号右移
上面在进行左移和右移时, 我有一点没讲到, 就是在对补码进行移动时, 符号位是固定不动的, 而无符号右移是指在进行移动时, 符号位也会跟着一起移动.
比如 - 2>>> 1.
-2 用原码表示为 10000000 00000000 00000000 00000010
-2 用反码表示为 11111111 11111111 11111111 11111101
-2 用补码表示为 11111111 11111111 11111111 11111110
-2 的补码右移 1 位为: 01111111 11111111 11111111 11111111
右移后的补码对应的反码, 原码为: 01111111 11111111 11111111 11111111 (因为现在的符号位为 0, 表示正数, 正数的原, 反, 补码都相同)
所以, 对应的十进制为 2147483647.
也就是 - 2>>> 1 = 2147483647
总结
文章写的可能比较乱, 希望大家能看懂, 能有所收获. 这里总结一下, 我们可以发现:
- 2 << 1 = 4 = 22
- 2 << 2 = 8 = 222
- 2 << n = 22
- m << n = m * 2
右移则相反, 所以大家以后在源码中再看到位运算时, 可以参考上面的公式.
来源: https://www.cnblogs.com/nicerblog/p/11348608.html