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)数字,加l或L。
但有一点很特殊的,对于十进制,要表示负数的话,必须明确地使用负号-,而对八进制和十六进制却没有要求,只要符号位是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曰,要尽量避免不同类型的数字的运算。
附
- JLS 3.10.1 Integer Literals
- JLS 5.1.2 Widening Primitive Conversion
- Integer和Long中的toBinaryString, toHexString几个方法比较好用。