(/≧▽≦)/~┴┴ 嗨~我叫小奥 ✨✨✨
👀👀👀 个人博客:小奥的博客
👍👍👍:个人CSDN
⭐️⭐️⭐️:Github传送门
🍹 本人24应届生一枚,技术和水平有限,如果文章中有不正确的内容,欢迎多多指正!
📜 欢迎点赞收藏关注哟! ❤️
问题引入
首先我们来看下面一组代码的执行结果
public static void main(String[] args) {
System.out.println(2.0f - 1.9f == 0.1f); // false
System.out.println(16777218.0f - 16777217.0f == 2.0f); // true
System.out.println(2.0d - 1.9d == 0.1d); // false
System.out.println(18_014_398_509_481_988d - 18_014_398_509_481_986d == 4d); // true
}
浮点数实现原理
浮点类型
浮点类型用于表示有小数的部分。在Java中有两种浮点类型,具体内容如表3-2表示。
一般情况应该尽量避免使用float。
浮点数中有效位数的含义:浮点数能精确表达的十进制的位数
1.123456 1位整数 + 6位小数 = 7位十进制数
1000.123 4位整数 + 3位小数 = 7位十进制数
16777218 共8位十进制数,前7位为有效位数,最后1位为无效位(float无法精确表达)
正确使用浮点数
(1)比较两个浮点数是否相等,不能直接使用 == 号(全等号)。
public static void main(String[] args){
double precision = 0.000001; // 10^-6
double d1 = 2.0d - 1.9d;
double d2 = 0.1d;
if (Math.abs(d1 - d2) < precision) {
// 在精度范围内,d1等于d2
System.out.println("true");
}
}
(2)尽量不要使用float,而是使用double。
(3)涉及到金额的场景,一定要使用BigDecimal类。
浮点数的原理
在计算机的底层,所有的数据都是由0和1存储的,也就是二进制,每个0和1都是一个bit,小数也是同样如此。
在计算机中,通常使用IEEE754标准来规定如何以二进制的方式表示存储十进制的数据。
以float单精度浮点数为例:
- 符号位:0表示负数,1表示整数
- 偏移指数位(biased exponent):用来表示规范化后的指数值
- 尾数位(fraction):用来表示规范化后的尾数值
IEEE754规范化
十进制:10.75
二进制:b(1010.11)
规范化:b(1.01011) * 2^3,其中1.01011是尾数, 3是指数
偏移指数位
假设规范化后,指数值为E,
对于float:
E的取值范围规定是[-127, 128],为了避免负数,于是将E加上一个固定值的偏移量127. E' = E + 127, E'的取值范围是[0, 255],E'即“偏移指数”,用“偏移指数位”中的8个比特来表示
十进制:10.75
规范化:b(1.01011) * 2^3
指数值:3
偏移指数值: 3 + 127 = 130 (加上偏移量,避免出现负数)
偏移指数位: 1000 0010 (8位比特的二进制)
尾数位
假设规范化后,尾数为F
对于float:
尾数F最多使用23位比特来存储。F的最高位肯定是1,为了增加精度,在存储时,我们可以省略掉最高位,只存储小数后的数字
十进制:10.75
规范化:b(1.01011) * 2^3
尾数值:1.01011
尾数位:0101 1000 0000 0000 0000 000 (23位)
总结
将十进制10.75转化为二进制数:
再由二进制反推出计算机内十进制的表示:
最后浮点数10.75在计算机内表示为1.3125。
注意:二进制天生不能精确表达某些十进制数字
比如:
0.1 = 2^-4 + 2^-5 + 2^-8 + 2^-9 + 2^-12 + 2^-13+… B(0.000 110 011 001 100 …) 无限二进制表示
而在计算机中,浮点数的存储位数是有限的。
解释下最开始的代码问题
System.out.println(2.0f - 1.9f == 0.1f); // false
System.out.println(16777218.0f - 16777217.0f == 2.0f); // true
这也就是说float精度是7位有效位数的经典示例。
大数吃小数问题
public static void main(String[] args){
float s = 0f;
float x = 1f;
for(int i = 0; i < 3000_0000; i++) {
s += x;
}
System.out.printf("%1.10f\n", s);
}
// 输出
// 16777216.0000000000
下面我们讲解一下浮点数的相加过程:
- 指数位对齐,小的向大的对齐
- 尾数求和
- 规范化
以0.5 + 0.125
为例:
0.5 = (-1)^0 * 1.0 * 2^-1
0.125 = (-1)^0 * 1.0 * 2^-3
0.5的指数位-1,0.125的指数为-3,因此0.125的有效位需要右移,即 0.125 = (-1)^0 * 0.01 * 2^-1
然后相加 0.5 + 0.125 = (-1)^0 * 1.01 * 2^-1
从浮点数相加的过程我们知道,由于浮点数有效位是23位,当较大的数是较小数的2^24 = 16777216倍及以上的时候,较小的数为了将指数位向较大的数对齐,有效位就会右移,右移超过了23位,就会成为无效位。
解决方法:使用更高精度的数据类型,尽量避免较大数和较小数直接相加,或者通过补偿的方式。
(1)使用double代替float。
public static void main(String[] args){
double s = 0d;
double x = 1d;
for(int i = 0; i < 3000_0000; i++) {
s += x;
}
// 输出:30000000.0000000000
System.out.printf("%1.10f\n", s);
}
(2)分段求和。
public static void main(String[] args){
float s = 0f;
float s1 = 0f, s2 = 0f, s3 = 0f;
float x = 1f;
for(int i = 0; i < 1000_0000; i++) {
s1 += x;
}
for(int i = 0; i < 1000_0000; i++) {
s2 += x;
}
for(int i = 0; i < 1000_0000; i++) {
s3 += x;
}
// 输出:30000000.0000000000
s = s1 + s2 + s3;
System.out.printf("%1.10f\n", s);
}
(3)Kahan求和
思想:保存较大数和较小数相加过程中,较小数丢失的有效数字,然后补偿回来。
public static void main(String[] args){
// s 总和 eps 丢失的有效数字 t 临时和
float s = 0f, eps = 0f, t = 0f;
float x = 1f;
for(int i = 0; i < 3000_0000; i++) {
float y = x - eps; // y是被截断的小数
t = s + y; // s是大的,y是小的,所以y的低位数字会丢失
eps = t - s - y; // t - s 恢复y的高位部分, -y 恢复y的低位部分
s = t; // 保存总和s
// 下一次循环,丢失的部分将会尝试添加到y中
}
// 输出:30000000.0000000000
System.out.printf("%1.10f\n", s);
}