Java移位运算符-学习总结

Java移位运算符的相关知识,包括其作用在负数上的情况。

Java语言规范

原文:The Java® Language Specification Java SE 8 Edition - 15.19. Shift Operators

The shift operators are syntactically left-associative (they group left-to-right).

Unary numeric promotion (§5.6.1) is performed on each operand separately. (Binary numeric promotion (§5.6.2) is not performed on the operands.)

It is a compile-time error if the type of each of the operands of a shift operator, after unary numeric promotion, is not a primitive integral type.

The type of the shift expression is the promoted type of the left-hand operand.

If the promoted type of the left-hand operand is int, then only the five lowest-order bits of the right-hand operand are used as the shift distance. It is as if the right-hand operand were subjected to a bitwise logical AND operator & (§15.22.1) with the mask value 0x1f (0b11111). The shift distance actually used is therefore always in the range 0 to 31, inclusive.

If the promoted type of the left-hand operand is long, then only the six lowest-order bits of the right-hand operand are used as the shift distance. It is as if the right-hand operand were subjected to a bitwise logical AND operator & (§15.22.1) with the mask value 0x3f (0b111111). The shift distance actually used is therefore always in the range 0 to 63, inclusive.

At run time, shift operations are performed on the two’s-complement integer representation of the value of the left operand.

The value of n << s is n left-shifted s bit positions; this is equivalent (even if overflow occurs) to multiplication by two to the power s.

The value of n >> s is n right-shifted s bit positions with sign-extension. The resulting value is floor(n / pow(2, s)). For non-negative values of n, this is equivalent to truncating integer division, as computed by the integer division operator /, by two to the power s.

The value of n >>> s is n right-shifted s bit positions with zero-extension, where:

If n is positive, then the result is the same as that of n >> s.

If n is negative and the type of the left-hand operand is int, then the result is equal to that of the expression (n >> s) + (2 << ~s).

If n is negative and the type of the left-hand operand is long, then the result is equal to that of the expression (n >> s) + (2L << ~s).

The added term (2 << ~s) or (2L << ~s) cancels out the propagated sign bit.

Note that, because of the implicit masking of the right-hand operand of a shift operator, ~s as a shift distance is equivalent to 31-s when shifting an int value and to 63-s when shifting a long value.

注释:原文中的promotion指的是类型转换,promoted type指的是经过类型转换(包括隐式转换和显式转换)后的数据类型。

总结

根据Java规范对移位操作符的描述,可以总结为以下内容:
n << sn >> sn >>> s为例,其中n和s分别表示经过数据类型转换后(包括隐式转换和显式转换)的结果。

  • n和s只能是基础的整数的数据类型,包括byteshortcharintlong,不能是浮点型,否则会造成编译错误。
  • 移位运算式的结果的数据类型与n的数据类型相同。
  • 如果n的数据类型为int,那么只会截取s的二进制的低5位作为移位操作的距离,也就是说n << s等价于n << (s&0x1f),因此实际移位操作的距离范围为[0, 31]。
  • 如果n的数据类型为long,同理,只会截取s的二进制的低6位作为移位操作的距离,也就是说n << s等价于n << (s&0x3f),实际移位操作的的距离范围为[0, 63]。
  • 在运行时中,移位操作符的运算是在n的补码的基础上进行的。
  • n << s表示n向左移s位,结果相当于n乘以2的s次方(即使移位操作溢出)。
  • n >> s表示n带符号向右移s位,结果为小于(n/(2的s次方))的最大整数(例如结果为2.5则取2,结果为-2.5则取-3)。若n为正数,可以认为在代码里执行n >> s和执行n / Math.pow(2, s)的结果是一样的,负数则不然。
  • n >>> s表示无符号右移s位,结果分为以下几种情况:
    • 如果n为正数,结果相当于n >> s
    • 如果n为负数,且n的数据类型为int,则其运算结果结果为(n >> s) + (2 << ~s)
    • 如果n为负数,且n的数据类型为long,则其运算结果结果为(n >> s) + (2L << ~s)

除了以上原文提到的,还需要知道几点:

  • 在进行移位运算符之前,byteshortchar为被隐式转换为int
  • 原文有提到n是以为补码的形式进行移位操作的,也就是说n在移位之前会先转换成补码。经过我的实际测试,发现n和s在移位操作之前都会先转换为补码。当然在一般情况下,n和s都是正数,在补码系统中,正数的表示和原码一样。
  • >>在移位操作时会根据n的正负在高位进行补位,若n为正数,则每移动一位都会在高位补0,若n为负数,则每移动一位都会在高位补1。
  • >>>在移位操作时,不管n是正负都会在高位补0。

‘a’ << -10 = ?

1
2
3
4
5
public class Main {
public static void main(String... args) {
System.out.println('a' << -10); // output: 406847488
}
}

以上这个比较奇怪的例子是可以编译通过的,并且执行结果为406847488。
根据这个例子,可以来详细了解移位运算符在执行时到底干了什么。

  1. 首先在执行移位操作之前,会检查移位运算符两边的操作数的数据类型,如果有必要的话会进行隐式转换。因此左边char类型的’a’会隐式转换为int类型,即’a’会被转换成97,右边的-10为int类型,不需要转换。
  2. 经过数据类型转换后,因为移位操作是在补码的基础上执行的,所以会将97和-10都先转换为补码,分别是97补码(0000 0000 0000 0000 0000 0000 0110 0001)和-10补码(1111 1111 1111 1111 1111 1111 1111 0110)。
  3. 接着,因为左边的97为int数据类型,因此会截取-10补码的低5位,即(10110),等于22。
  4. 现在可以开始移位操作了,将97补码(0000 0000 0000 0000 0000 0000 0110 0001)向左移22位(包括符号位),空位用0补全,结果为(0001 1000 0100 0000 0000 0000 0000 0000),注意此时得出的结果还是补码,因此还要将补码转换为原码才是最后的结果。这里的补码为正数,因此与原码一致,转换结果依旧是(0001 1000 0100 0000 0000 0000 0000 0000),等于406847488。

同理,>>>>>执行过程也大致相同,只不过在移位时在高位补位的方式不同。

参考

The Java® Language Specification Java SE 8 Edition - 15.19. Shift Operators