问题

昨天同事和我聊了他偶然间看到的一道面试题,引出了后面的深入分析。

1
System.out.println(Integer.valueOf((char)-1)); //output: 65535

为什么输出是 65535 而不是 -1 呢?

分析

先回顾一下计算机相关的知识点:

原码(True form)

原码是指一个二进制数左边加上符号位后所得到的码,且当二进制数大于0时,符号位为0;二进制数小于0时,符号位为1;二进制数等于0时,符号位可以为0或1(+0/-0)。 –维基百科

原码根据符号位能很直观的看出是正数还是负数,但是对于计算机运算来说却不是那么好用了,比如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 求解: 1 + (-1) 
---
站在人类的视角会立马得出 1 + (-1) = 0 的结论
---
而计算机的角度(假设长度是8-bit):
转换成二进制并相加:
  00000001
+ 10000001
 ----------
  10000010
 
这个结果是:-2,与正确值不符

所以如果有负数使用了原码运算,那还得把符号位撇开,这就增加了使用成本,为了解决这个问题就出现了反码。


反码(ones' complement),也叫一的补码

二进制数每个数字反转,得到的数即为原二进制的一的补码(英语:ones' complement)。若某一位为0,则使其变为1,反之亦然。

  • 一的补码以有符号比特的二进制数定义。
  • 一的补码是有符号比特的二进制数。
  • 正数和0的一的补码就是该数字本身。–维基百科

反码能解决负数运算的问题吗,可以,但是又会出现负零循环进位的问题。举例:

1
2
3
4
5
6
7
8
9
# 求解  1 + (-1)
---
转换成二进制并取反相加
  00000001 (正数的反码是它本身)
+ 11111110 (符号位不变其余取反)
------------
  11111111 
转换成原码为:100000000 = -0
  

对于 0 来说正负并没有意义, 而且还会有反码 0000 0000 和 1111 1111 这两种 0 的表现形式,在使用反码运算时判断是否为 0 还需要考虑这两种情况,为了解决这个问题就又出现了补码。


补码(2’s complement)

正数和0的补码就是该数字本身。负数的补码则是将其对应正数按位取反再加1。

补码系统的最大优点是可以在加法减法处理中,不需因为数字的正负而使用不同的计算方式。只要一种加法电路就可以处理各种有号数加法,而且减法可以用一个数加上另一个数的补码来表示,因此只要有加法电路及补码电路即可完成各种有号数加法及减法,在电路设计上相当方便。

另外,补码系统的0就只有一个表示方式,这和反码系统不同(在反码系统中,0有二种表示方式),因此在判断数字是否为0时,只要比较一次即可 –维基百科

补码有一个原则:数字a的补码为 -a,举例:

1
2
3
4
# a = -5,那么 a 的补码为 5
10000101  # -5 的原码
11111010  # -5 的反码
11111011  # -5 的补码 

解答

上面的问题可以拆分成下面的代码便于分析

image-20200707094407456

int(32-bit) 转 char(16-bit) 会失去精度

1
2
3
4
1000 0000 0000 0000 0000 0000 0000 0001   #int -1 的原码
1111 1111 1111 1111 1111 1111 1111 1111   #int -1 的补码(内存中的数值用补码表示 0Xffffffff) 
                    1111 1111 1111 1111   #char 只保留 16 位;10进制为 65535
0000 0000 0000 0000 1111 1111 1111 1111   #再转换成 int 执行零扩展;10进制同样为 65535             

扩展

溢出

符号扩展与零扩展

循环进位