Skip to main content

数字的存储

huhxAbout 7 minjavaJava

数乃万物之理,了解数字,就是掌握真理。都知道数字在计算机中是二进制存储的,但是具体到存储细节可能让很多程序员犯难了。这里我们就深耕细节,展开介绍下整数及浮点数在java虚拟机的表示。

概念介绍

计算机的信息都是以二进制形式表示的,数值也不例外。数有正负之分,那就把最高位存放符号位呗(0为正,1为负),这诞生了原码。

什么是原码

所谓原码,就是符号位加上数字的二进制表示,其中符号位1表示负数,0表示正数。比如说该整数类型的位数是8,那么原码能够表示该整数类型数值的范围就是:(-27+12^7 + 1) ~ (2712^7 - 1) => (-127 ~ 127)

好了,数值有了在计算机中的表示方法,那么就可以对数值进行算术运算了。但是很快就发现原码进行乘除运算时还好,在加减运算的时候就出现了幺蛾子。

假定一个数是8位,我们来看看1 - 1的计算过程:

1 - 1 = 1 + (-1)
00000001(原码) + 10000001(原码) = 10000010(原码) => -2

这就尴尬了,计算机连最简单的1 - 1都能算错🙄。那让它算下 1 + 1?

00000001(原码) + 00000001(原码) = 00000010(原码) => 2

好吧,没问题。事实上对于正数的加法运算中原码是完全ok的,问题就出现在带符号位的负数身上。于是,人们为了解决这个问题,就发明了....

什么是反码

对,没错,就是反码。反码对于正数就是原码本身,对于负数则在原码的基础上,符号位不变,其余位取反。好的,那来验证下上面的两个运算:

// 1 - 1
1 - 1 = 1 + (-1)
00000001(反码) + 11111110(反码) = 11111111(反码) = 10000000(原码) = -0  // 有些小问题

// 1 + 1
00000001(反码) + 00000001(反码) = 00000010(反码) = 00000010(原码) = 2  // ok

这一次加减法是没啥子问题,但是有瑕疵,那就是出现在0这个特殊数值上。+0-0虽说数值是一样的,但是给0带上符号是没意义的。而且0的编码表示还存在两种: 10000000(-0)和00000000(+0)。好了,人们还是接受不了0的这个问题,于是补码粉墨登场😎

什么是补码

什么是补码?简单来说:负数的补码就是反码加1,整数的补码就是原码本身。那使用补码再来看看上面的1 - 1的计算

// 1 - 1
1 - 1 = 1 + (-1)
00000001(补码) + 11111111(补码) = 00000000(补码) = 00000000(原码) = 0

那关于0的补码表示呢?

  • 如果0是正数,补码为原码本身:00000000
  • 如果0是负数,原码是10000000,反码为11111111,补码为反码加上则是00000000

这样补码中0的表示就只有一种: 00000000,之前说的-0则不存在了,而且补码中的10000000还可以表示最低数-128,这样8位的数值范围就是:(-128 ~ 127)

数值的表示总算尘埃落定,补码不负众望,成为了计算机中的数值表示

Info

  1. 原码是最符合人直观的数值二进制表示,但是存在减法运算的问题
  2. 反码在原码基础之上,解决了减法运算的问题,但是又存在0有两个编码的问题
  3. 补码在反码基础之上,解决了0有两个编码的问题,也多出了一个编码表示最低数。同时也简化整数的加减法计算,将减法视为加法,实现加减法的统一

好了,我们讲了数值二进制的几种编码,原码反码补码。那具体到java中,又是有哪些的不同呢?

整数的表示

数值有大小之分,为了方便存储与计算效率,在java虚拟机中,整数分为byte,short,intlong四种,分别表示8位、16位、32位、64位有符号整数。java中是不存在无符号整数的,C语言是有的。具体如下:

基本类型大小(bit)最小值最大值
byte8-128127
short16-2152^{15}2152^{15} -1
int32-2312^{31}2312^{31} -1
long64-2632^{63}2632^{63} -1

java中Integer和Long类型也提供了输出二进制补码的支持,拿Integer来说:

Integer.toBinaryString(10); // 1010
Integer.toBinaryString(-10); // 11111111111111111111111111110110

浮点数的表示

在编程中,浮点类型数据主要用于表示小数的。对比于整数的表示,计算机表现小数的难点就在于小数点后面的数字了。浮点数在计算机中的表示是基于科学计数法的,只不过是二进制的而已。

我们知道二进制表示整数的时候,最低位表示202^0,往高位依次是212^1,222^2,232^3,...,2n2^n。那么对应的,对于二进制小数点的部分,最高位则是212^{-1},222^{-2},232^{-3},242^{-4},...,2m2^{-m}

下面举几个例子:

calculate
calculate

十进制的7.125,用二进制表示是111.001。其实这种转换是存在些问题的,十进制不是无限循环小数的,转换成二进制就变成了无限循环小数。像上述例子中的0.6,表示成二进制之后成了循环体为1001的无限循环小数。而计算机底层无法精确存储那个无限循环二进制数的,只能存一个四舍五入的数值了,这就导致了浮点数精度不准确的问题。这种机制也直接说明了:对于金额这种对精度有要求的计算,可别用浮点数了。

下面是java中的具体例子, float数值的比较和运算都有可能会出现问题:

0.60000003f == 0.60000001f // true
0.1f + 0.11f // 0.21000001

下一步,将二进制表示为以2为底的科学计数法,如图:

transform
transform

所以要存浮点数数,需要存储三个部分:正负号,尾数,指数。二进制的科学记数法表现形式如下

value=flagm2n value = flag * m * 2^{n}

Info

我们知道-32767这个数用科学计数法可以写成-3.2767×10410^{4},其中-表示符号,3.2767称为尾数,4称为指数。浮点数在计算机中的表示与此类似,只是它的基数是2而不是10。

所以再看上面的公式:flag * m * 2n2^{n}就容易得知,flag就是符号位,m就是尾数,n就是指数了,由于是二进制的科学计数法,此处的m的范围:1.0 <= m < 2.0,数值都是1点几了,1可以不存,只存小数点后面的数字了。

知道了这三个是浮点数表示的重要组成部分,我们再来看下IEEE 754open in new window标准了,它是浮点数的主要存储方案。这一标准最早在1985年提出,基本上已经被用于所有计算机中。先后经历了几次更新,但浮点数的表示规则却从来没有变过。

在IEEE 754的定义中,一个浮点数由3部分组成,分别是符号位、指数位和尾数位。Java中floatdouble两种类型的表示如下:

类型符号位指数位尾数位总共
float182332
double1115264

对于指数部分,加上了编译量2n12^{n} - 1,所以对于8位的float类型,编译量就是127,相应的double类型就是1023。

所以对于float类型的-7.5,我们来看看它的二进制存储是什么样的

浮点数:-7.5f
浮点数:-7.5f

我们快速算下0.6的数值:

浮点数:0.6f
浮点数:0.6f

Java本身是平台无关的,它提供了统一的字节序视图,不受底层硬件的影响。Java虚拟机规定了所有数值类型的大端字节序

float浮点数还可以表示一些特殊的数字,如下:

数值二进制表示
正无穷0 11111111 00000000000000000000000
负无穷1 11111111 00000000000000000000000
NaN0 11111111 10000000000000000000000
00 00000000 00000000000000000000000
最大正浮点数0 11111110 11111111111111111111111
最小正浮点数0 00000000 00000000000000000000001

FAQ

使用补码,怎么乘除法?

字节序的大端模式与小端模式?

浮点数正无穷与负无穷?

浮点数为什么要加偏移量?

总结

参考资料