整数篇
我们都知道计算机只能识别二进制,大学里我们都学过二进制和十进制之间的转换,下面用几个例子来解释(以下大部分场景用Java代码演示,使用4个字节共32位的int)
由于Java中的int是有符号的,所以32位不能全部用来表示数字,得用最高位来表示正数还是负数(1是负数,0是正数),剩下的31位用来表示数字。
正整数
十进制->二进制
我们用43
这个数字举例(原谅我没找到markdown中的语法,只能用截图代替)
高位都用0补齐
正
0 0000000 00000000 00000000 00101011
二进制->十进制
正
0 0000000 00000000 00000000 00101011
从右往左
我们在用工具校验下,没毛病!
image.png
负整数
十进制->二进制
负整数
我们使用-11
举例,首先符号位是1,然后11的二进制数表示是00001011
,所以完整的是
负
1 0000000 00000000 00000000 00001011
那-11
是上面那串数字吗?错!
这里需要引入另一个概念,原码
、反码
、补码
,计算机中都是以补码
的形式进行存储的。
那为什么正整数
的时候不说这个概念呢?
因为正整数
的原码
=反码
=补码
三个都是相等的
但是负整数
是不一样的,上面那一串其实是-11
的原码
原码
到反码
的过程是:
符号位不变,其余所有位取反
原码:1 0000000 00000000 00000000 00001011
反码:1 1111111 11111111 11111111 11110100
而反码
到补码
的过程是:
符号位不变,直接+1
反码:1 1111111 11111111 11111111 11110100
补码:1 1111111 11111111 11111111 11110101
// 11111111111111111111111111110101 和上面计算的结果是一样的
System.out.println(Integer.toBinaryString(-11));
有了补码
,想得到原码
,有两种方法
第一种:只需要将上面的操作反过来就行
而补码
到反码
的过程是:
符号位不变,直接-1
补码:1 1111111 11111111 11111111 11110101
反码:1 1111111 11111111 11111111 11110100
而反码
到原码
的过程是:
符号位不变,其余所有位取反
反码:1 1111111 11111111 11111111 11110100
原码:1 0000000 00000000 00000000 00001011
第二种:用原码到补码的过程再做一遍也行
补码
到反码
的过程是:
符号位不变,其余所有位取反
补码:1 1111111 11111111 11111111 11110101
反码:1 0000000 00000000 00000000 00001010
而反码
到原码
的过程是:
符号位不变,直接+1
反码:1 0000000 00000000 00000000 00001010
原码:1 0000000 00000000 00000000 00001011
是不是很神奇呢
二进制->十进制
先得把存储在计算机中得补码
转换成原码
,再将原码
转换成十进制
补码:1 1111111 11111111 11111111 11110101
原码:1 0000000 00000000 00000000 00001011
然后符号位单独看,1011
就是十进制的11
,就能得到-11
负
1 0000000 00000000 00000000 00001011
溢出
在看浮点数之前,先看看整数溢出的情况
代码如下:
int i = Integer.MAX_VALUE;
// 2147483647
System.out.println(i);
int j = i + 33;
// -2147483616
System.out.println(j);
变量i
已经是int的最大值了,如果在增加33,会得到什么呢?答案很明显,原本的正数变成了负数
下面我们用二进制的视角看下计算过程
# i
正
0 1111111 11111111 11111111 11111111
# 33 换成二进制
正
0 0000000 00000000 00000000 00100001
将两个数相加
0 1111111 11111111 11111111 11111111
+ 0 0000000 00000000 00000000 00100001
————————————————————————————————————————
1 0000000 00000000 00000000 00100000
我们前面说过了最高位是符号位,所以相加后的结果,计算机会识别为负数,而且是补码
,所以要知道这串二进制代表的是哪个负数,需要把补码
转成原码
,用上面提到的两个方法中的一个就行
补码:1 0000000 00000000 00000000 00100000
反码:1 1111111 11111111 11111111 11011111
原码:1 1111111 11111111 11111111 11100000
最终的结果,不看符号位代表的十进制
数是2147483616
,加上符号就是-2147483616
,和代码结果是一致的。
用工具验证下
再举一个例子,对int最小值做减法
int i = Integer.MIN_VALUE;
// -2147483648
System.out.println(i);
int j = i - 33;
// 2147483615
System.out.println(j);
和预期一样,负数变成了正数
下面我们用二进制的视角看下计算过程
# i
负
1 0000000 00000000 00000000 00000000
# 33 换成二进制
正
0 0000000 00000000 00000000 00100001
将两个数相减
1 0000000 00000000 00000000 00000000
- 0 0000000 00000000 00000000 00100001
————————————————————————————————————————
0 1111111 11111111 11111111 11011111
符号位是0,这就是一个正数,所以直接将这个转成十进制
数是2147483615
,和代码结果是一致的。
用工具验证下
浮点数篇
在进入浮点数讨论前,先请大家思考一个问题,由于之前整数部分是以int
作为示例的,所以浮点数也同样以float
作为示例,因为在Java中都是4个字节的。
来自百度百科对int
的解释
int占用4字节,32比特,数据范围为-2147483648到2147483647
来自百度百科对float
的解释
float占用4字节,32比特,数据范围为-3.4E+38 ~ 3.4E+38
那么问题来了,同样是4个字节的,只有0和1,一共32位,能表示的数字最多就是个数字,为什么float
比int
表示的范围要大?
首先先了解下float
的32个字节是如何分配使用的
符号位(S):1bit | 指数位(E):8bit | 尾数位(M):23bit |
---|
- 符号位S:和
int
一样,0表示正数,1表示负数 - 指数位E:8位的二进制能表示的范围是0~255
- 尾数位M:实际存储的小数
然后还需要了解一个公式(IEEE 754)
公式分为3部分
- 第一部分:很好理解,用一个系数
1
或者-1
去控制整个数的正负 - 第二部分:,刚刚说了E的取值是0~255,所以这个部分的范围就是 到
- 第三部分:最复杂的就是这个M了,如果使用二进制表示浮点数,可以是做一点转换再做一点转换,但是计算机在将该数存储之前会做一个
规格化
,规格化
指的就是把小数点都移动到第一位和第二位之间,像这样但是之后的指数就要修改来保证数字大小并没有发生改变,规格化
的好处就是在于不用去记录小数点的位置。这里2的指数被记做e
,e=E-127
,例子中e=3
,所以E=130
,规格化
后必须保证整数部分是1
,也就是1.xxxxxxxxx
。
但是实际的情况比这个又要再复杂一点点
含义 | 符号位(S):1bit | 指数位(E):8bit | 尾数位(M):23bit |
---|---|---|---|
规格化后整数部分是1 | 0:正,1:负 | 0<E<255 | 规格化后的小数部分 |
规格化后整数部分是0 | 0:正,1:负 | 0 | 规格化后的小数部分 |
无穷大 | 0:正,1:负 | 255 | 0 |
NaN | 0:正,1:负 | 255 | 不等于0 |
所以纠正下之前的一个关于E的结论,由于设计上E等于0或者255,整个float是算做特殊情况的,所以不算特殊情况的话E的取值是1~254,所以这个部分的范围就是 到
举例说明:
float f = 33.25F;
int bits = Float.floatToIntBits(f);
// 0 10000100 00001010000000000000000
System.out.println(Integer.toBinaryString(bits));
首先将该数转换成二进制,那么如何把一个浮点数转成二进制呢?
我们先把一个浮点数
,拆分成整数部分
和小数部分
,
整数部分
的33
转换和之前介绍的一样,二进制表示为100001
小数部分
的0.25
转换则略微有些不一样
将小数部分
不停的乘以2,取乘积结果的整数部分作为二进制数位,直至乘积结果小数部分消失。
例如:
0 | 0.25 * 2 = 0.5
1 | 0.5 * 2 = 1
所以,小数部分
的0.25
,二进制表示为01
。
整合起来就是100001.01
这里需要提一嘴的是,不是所有小数都能在有限位数中被精确的表示成二进制数的,绝大部分小数都不行,所以这就是计算机无法精确表示小数的最主要原因。
// 33.25
100001.01
100001.01 * 1
100001.01 * 2^0
// 规格化,这里的 e=5
1.0000101 * 2^5
// E = e + 127
E = 5 + 127 = 132
// 132转换成二进制数就是 指数位的二进制表示
10000100
// M部分就是,规格化后的小数部分+右边全部补零至23位
00001010000000000000000
// 把整个结果拼起来,和java代码展示的结果是一致的
符号位 指数位 尾数位
0 10000100 00001010000000000000000
举例说明2:
float f = 0.25F;
int bits = Float.floatToIntBits(f);
// 0 01111101 00000000000000000000000
System.out.println(Integer.toBinaryString(bits));
首先同样将该数转换成二进制
// 0.25
0.01
0.01 * 1
0.01 * 2^0
// 规格化,这里的 e=-2
1.0 * 2^-2
// E = e + 127
E = -2 + 127 = 125
// 125转换成二进制数就是 指数位的二进制表示
01111101
// M部分就是,规格化后的小数部分+右边全部补零至23位
00000000000000000000000
// 把整个结果拼起来,和java代码展示的结果是一致的
符号位 指数位 尾数位
0 01111101 00000000000000000000000
举例说明3:
float f = -17.125F;
int bits = Float.floatToIntBits(f);
// 1 10000011 00010010000000000000000
System.out.println(Integer.toBinaryString(bits));
// -17.125
10001.001
10001.001 * 1
10001.001 * 2^0
// 规格化,这里的 e=4
1.0001001 * 2^4
// E = e + 127
E = 4 + 127 = 131
// 131转换成二进制数就是 指数位的二进制表示
10000011
// M部分就是,规格化后的小数部分+右边全部补零至23位
00010010000000000000000
// 把整个结果拼起来,和java代码展示的结果是一致的
符号位 指数位 尾数位
1 10000011 00010010000000000000000
所以回到开头的问题,为什么同样是4个字节32位,float
比int
能表示的数字范围要大很多。
int
能表示的数字范围中的每一个整数,它都能正确的用二进制表示出来。
float
则不能,哪怕抛开浮点数不看,让float
只表示整数的话,在它的范围中也有许多数字它是
无法精确表示的,更不要说加了小数位后了。
同样的解释,也能用于long
和double
了
最后,再抛出一个问题:为什么BigDecimal能‘精确’的表示小数呢
网友评论