基础数据类型背后的二进制是怎么回事?

246 阅读5分钟

1. 前言

大家好我是biubiuQ😎,作为一名程序员,基础数据类型的使用在我们日常开发中无处不在,选用适合的数据类型,能让我们的程序中减少一些不必要的bug,在初学Java时,肯定会对下面的这几部分代码产生一些疑问:

  1. 当我们在计算小数的时候,通常会使用float和double类型,但是计算如下代码的时候,打印出的结果好像跟我们想要的不一样。
float f = 0.1f * 0.1f
System.out.println(f);

0.1乘0.1应该结果是0.01,可是打印出的结果却是0.010000001。有人会说应该用double类型,可是使用double后,结果成为更离谱的0.010000000000000002

  1. 我们在用int进行整数计算时,也有可能会碰到计算溢出的情况,我们都知道int类型是4字节的,他的最大值是2147483647,当这个值再加一的时候,会发生什么?
int res = 2147483647 + 1;
System.out.println(res);

运行后,控制台打印的结果是-2147483648,好端端的正整数,怎么还加出来负整数了呢? 带着这些疑问🤔️,我们来了解了解这背后到底是怎么回事。

2. 整数背后的运算逻辑

2.1 正整数的二进制表示

在我们的日常生活中,我们采用的是十进制,也就是满10进一位,比如10 + 1 = 11, 用0-9来展示数字。但是在计算机中,计算机采用的是二进制,二进制顾名思义,就是满2进一位。他只认得0和1,用0和1的组合来展示出所有的数字。比如int类型的1用二进制表示就是 00000000 00000000 00000000 00000001。每个数位表示某个数值的倍数。从右到左,每个数位的权重依次增大 2 倍。1就是2的0次方。

最近火热的电影《孤注一掷》中,男主靠6这个数字向外界传递求救信号,而六的二进制,正是 00000000 00000000 00000000 000001102^1 + 2^2 = 6。也就是110。

2.2 负整数的二进制表示

在生活中,使用十进制来表示一个负数,就是向一个整数前面加'-'号来表示,比如负一就是-1。 而在计算机中,我们通常用最高位来表示符号位,用0表示正数,1表示负数。我们用Integer.toBinaryString()方法去控制台打印下-1看看结果: 11111111 11111111 11111111 11111111,诶?最高位是符号位的话,-1不应该是10000000 00000000 00000000 00000001 这样表示吗?

带着疑问,为了方便理解,下面我们使用byte类型来进行运算,假如使用我们直觉中的10000001来表示-1,那么与1相加的话,会产生什么结果?

    1 00000001
   -1 10000001
   + ----------
   -2 10000011

我们直觉中的1 -1计算出来的是却是-2,这是怎么回事?

其实平时我们看到的是原码表示法,负数使用的是补码表示法。如果想要用原码表示法表示负数,会存在以下问题;

  • 会出现正0和负0的问题,不能进行加减运算。

只有转换成补码,计算机才能进行正常的加减运算,通常将一个整数转换成负数,会有以下三步:

  1. 写出该数的原码
  2. 所有位置取反变为反码
  3. 反码加1变成补码

下面用1到-1来演示这几步过程:

  1. 列出1的原码00000001
  2. 原码取反获得反码11111110
  3. 反码加一获得补码11111111

-1的二进制表示,就是通过以上步骤产生的。 下面我们再看看用补码来计算1 - 1

    1 00000001
   -1 11111111
   + ----------
   0  00000000

结果正是我们想要的0。

2.3 小数的二进制表示

在前言中,我们计算0.1 * 0.1的时候,没有得到我们想要的结果,这其实不是计算出了问题,而是计算机使用二进制不能精确的表示小数,他只能用一个十分接近的数来表示0.1。那又会有人问了,0.1 + 0.1算出的却是0.2,怎么没有出现精度缺失的情况呢?

     System.out.println(0.1f + 0.1f);//0.2
     System.out.println(0.1f * 0.1f);//0.010000001

这里在《Java编程的逻辑》一书中是这么解释的:

第一行输出0.2,第二行输出0.010000001。按照上面的说法,第一行的结果应该也不对。其实,这只是Java语言给我们造成的假象,计算结果其实也是不精确的,但是由于结果和0.2足够接近,在输出的时候,Java选择了输出0.2这个看上去非常精简的数字,而不是一个中间有很多0的小数。在误差足够小的时候,结果看上去是精确的,但不精确其实才是常态。

几乎所有的编程语言中,小数的格式都是遵循IEEE 754标准来表示的,标准定义了单精度(32位,float)和双精度(64位,double)浮点数的表示和操作。

  • 32位分布如下

    • 第1位:符号位置
    • 第2~9:指数位
    • 第10~32 :尾数位
  • 64位分布如下

    • 第1位:符号位置
    • 第2~12:指数位
    • 第13~64 :尾数位

我们可以使用以下代码来看浮点数的具体二进制展示是什么样的:

        Integer.toBinaryString(Float.floatToIntBits(value))
        Long.toBinaryString(Double.doubleToLongBits(value));

通常在开发过程中,如果我们要进行小数计算,还是推荐使用BigDecimal类来进行高精度的计算。

感谢阅读,如有问题请在评论区中指正😄