Puzzle 5: The Joy of Hex

没有负号的负数

在Java里,对于整数,有三种不同的数进制表达方式:

  • 八进制(Octal)
  • 十进制(Decimal)
  • 十六进制(Hexadecimal)

按照JLS 3.10.1的说法,它们的表达方式都是:前缀+允许的数字+后缀

  • 前缀:
    • 八进制 0
    • 十进制 没有
    • 十六进制 0x或0X
  • 允许的数字:
    • 八进制 0, 1, 2, 3, 4, 5, 6, 7
    • 十进制 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
    • 十六进制 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, a, b, c, d, e, f, A, B, C, D, E, F
  • 后缀:如果是长整型(Long)数字,加lL

但有一点很特殊的,对于十进制,要表示负数的话,必须明确地使用负号-,而对八进制和十六进制却没有要求,只要符号位是1就行(遵循补码的表达方式),负号只用于表示它后边的数值的相反数。这样,在Java中用八进制或十六进制表示负数时,就有可能出现标题中所说的”没有负号的负数“,自然,还会出现带了负号的正数。

比如:0xF2345678用十进制表示即为:-231451016(可直接使用System.out.println(0xF2345678);打印验证)。0xF2345678即为-231451016在Java中的二进制补码表达的等价写法。

  • 十六进制 0xF2345678
  • 二进制 11110010001101000101011001111000
  • 八进制 36215053170

以上。

书中提到的例子是考察如下语句的输出:

System.out.println(Long.toHexString(0x100000000L + 0xcafebabe));

相比应该输出1cafebe这个字串吧,但事实上输出的却是cafebabe

原因之一就是上面提到的十六进制对负数的表达,之二就是混合类型的运算——看看加号左边是一个Long型数,右边是一个Integer型数。0xcafebabe是一个负数,从int转成long的时候,符号位要扩展,结果就从0xcafebabe扩展成了0xffffffffcafebabeL.

书中的加法运算的竖式干脆抄过来,十分形象:

0xFFFFFFFFCAFEBABEL

+ 0x0000000100000000L

= 0x00000000CAFEBABEL

要得到1cafebe的话,只需让加号右边的操作数也为Long就行:

System.out.println(Long.toHexString(0x100000000L + 0xcafebabeL));

没有了从int到long的转化,竖式就变成了:

0x00000000CAFEBABEL

+ 0x0000000100000000L

= 0x00000001CAFEBABEL

简单地说:0xcafebabe和0xcafebabeL表达的数值不相等,所以导致了意外的输出。

  • 0xcafebabe = 11001010111111101011101010111110 = -889275714
  • 0xcafebabeL = 0000000000000000000000000000000011001010111111101011101010111110 = 3405691582
  • (long) 0xcafebabe = 1111111111111111111111111111111111001010111111101011101010111110 = -889275714

关于元数字类型的量转型的问题,可以参考附2. Java Puzzler曰,要尽量避免不同类型的数字的运算。

  1. JLS 3.10.1 Integer Literals
  2. JLS 5.1.2 Widening Primitive Conversion
  3. Integer和Long中的toBinaryString, toHexString几个方法比较好用。