<<, >>, >>> 这些符号什么意思?有哪些容易被遗漏的细节?
上次介绍了JAVA中有趣的位运算 ,知道了位运算是直接对一个整形的二进制位进行操作,效率上比起加减乘除高不少,因此常运用在对性能很敏感的场景。
今天介绍在二进制下的移位操作。
原码、反码、补码 磨刀不误砍柴工,这几个名词可还有印象?
原码: 二进制表示,最左边的一位是符号位,0表示正数,1表示负数
反码: 正数时同原码,负数时,等于原码每位取反(除了符号位)
补码: 正数时同原码,负数时,等于反码+1
在计算机系统中,数值一律用补码来表示和存储。原因在于,使用补码,可以将符号位和数值域统一处理;同时,加法和减法也可以统一处理。此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路。
JAVA中也一样,存储和移位操作的都是补码,正数时都一样,负数时就要注意了。
<< 左移位 在二进制格式下,把所有的数字向左移动指定位数,左边的高位移出(舍弃),右边的低位多出来的空位补0。
n = n << 1,左移一位,相当于 n = n * 2
需要注意的是,正数的二进制最高位是0,如果左移后被怼上来的那位是1,这个数就成了负数。
如果觉得奇怪,想想有时候我们遇到过的场景:一个很大的int正数,乘一个正数后如果结果超过了int能存储的极限,往往就变成了负数,或者一个很小的正数。
另一个需要注意的地方,由于Java只存储补码,正数补码和原码相同先不管,负数的补码会把原码的0变成1,所以负数左移位时,移出去的最高是1,后面怼上来的一般也是1(没到极限),所以还是负数。
对于程序员,或许还是把内容和代码放在一起更容易让人注意...
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 public class Bit { public static void main (String[] args) { int n = 6 ; System.out.println(n + "左移1位 " + (n << 1 )); System.out.println(n + "左移2位 " + (n << 2 )); System.out.println(n + "左移3位 " + (n << 3 )); int x = 0b01000000000000000000000000000001 ; System.out.println("x = " + x); System.out.println("x左移1位\t= " + (x << 1 )); System.out.println("x乘2\t= " + (x * 2 )); System.out.println("x左移2位\t= " + (x << 2 )); System.out.println("x乘4\t= " + (x * 2 * 2 )); int y = -3 ; System.out.println(y + " 二进制表示(补码) " + Integer.toBinaryString(y)); System.out.println(y + " 左移1位 " + (y << 1 )); } }
>> 右移位 在二进制格式下,把所有的数字向右移动指定位数,低位移出(舍弃),高位的空位补符号位,即正数补0,负数补1(想想负数存的补码和原码是不同的)。
n = n >> 1,右移一位,相当于 n = n / 2 (PS:正数时)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class Bit { public static void main (String[] args) { int n = 6 ; System.out.println(n + "右移1位 " + (n >> 1 )); System.out.println(n + "右移2位 " + (n >> 2 )); System.out.println(n + "右移3位 " + (n >> 3 )); System.out.println(n + "右移4位 " + (n >> 4 )); } }
上面是正数右移,负数的时候情况又有点不同了。
由于计算机存储和位移的都是补码,正数补码和原码一样,一直右移最后都变成了0,就像一直整除2,最后不管怎么除都是0。
而负数的补码一直右移最后全都是1,即:
1 2 3 4 补码: 11111111111111111111111111111111 反码: 11111111111111111111111111111110 (补码-1) 原码: 10000000000000000000000000000001 十进制: -1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class Bit { public static void main (String[] args) { int m = -3 ; System.out.println(m + "\t补码 " + Integer.toBinaryString(m)); System.out.println(m + "\t补码右移1位 " + (m >> 1 )); System.out.println(m + "\t补码右移2位 " + (m >> 2 )); System.out.println(m + "\t补码右移3位 " + (m >> 3 )); } }
>>> 无符号右移 依然是右移指定位数,与右移不同的是,无论正负,高位均补0。对于正数没影响,对于负数来说,这样一移,直接变成正数了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class Bit { public static void main (String[] args) { int m = -3 ; System.out.println(m + "\t补码 " + Integer.toBinaryString(m)); System.out.println(m + "\t补码无符号右移1位 " + (m >>> 1 )); System.out.println(m + "\t补码无符号右移2位 " + (m >>> 2 )); System.out.println(m + "\t补码无符号右移3位 " + (m >>> 3 )); } }
<<< 无符号左移
位数限制 一个容易忽略的地方,每次移动一位循环N次,和一次移动N位,结果并不一定是一样的。
以int为例,如果直接左移36位,结果并不是0,而是等同于左移36%32=4位。
右移和无符号右移也同样适用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class Bit { public static void main (String[] args) { int m = 3 ; int t = m; for (int i = 1 ; i <= 36 ; i++) { t = t << 1 ; } System.out.println(t); System.out.println(m << 36 ); System.out.println(m << (36 % 32 )); } }
总结
箭头朝哪边,就往哪边移位
左移操作相当于乘2,右移相当于除2,不全是
左移操作可能改变正负,因为符号位会被移走,新符号位不一定和以前一样
右移操作不改变符号,因为左边填充的是符号位
无符号右移会把负数变成正数
没有无符号左移
位移超过JAVA基本类型的位数后,等同于位移取模后的位数