浮点数为什么会丢失精度?BigDecimal为什么不会?

前言

学java的肯定都知道,要保证小数运算精度不丢失我们得用BigDecimal对象。这篇文章就分析一下为什么用浮点数会造成精度丢失?BigDecimal是怎么解决精度丢失问题的?下面我们一起看看吧!

浮点数的表示

浮点数在计算机中通常采用 IEEE 754 标准进行表示。这个标准将数值分为三个部分:符号位、指数部分和尾数部分。由于尾数的位数有限,某些小数(尤其是十进制小数)无法精确地用二进制表示。

精度丢失案例

java 复制代码
public class FloatPrecision {
    public static void main(String[] args) {
        double a = 0.1;
        double b = 0.2;
        double c = a + b;
        System.out.println(c); // 输出 0.30000000000000004
    }
}

在这个例子中,尽管我们期望 c 的值为 0.3,但实际上它被计算为 0.30000000000000004。这种现象在浮点数运算中非常常见,尤其是在涉及多个浮点数的加减乘除时,误差可能会逐渐累积。

为什么浮点数不能精确表示小数呢?

了解了10进制小数转二进制过程,就知道为啥呢?

十进制小数转换成二进制小数采用"乘2取整,顺序排列法:

● 用2乘十进制小数,可以得到积

● 将积的整数部分取出,再用2乘余下的小数部分,又得到一个积

● 再将积的整数部分取出,如此进行,直到积中的小数部分为零,此时0或1为二进制的最后一位。或者达到所要求的精度为止。

0.00011001100110011...(循环)

BigDecimal 的解决方案

通过BigDecimal("0.1") 的String 构造不会造成精度丢失,避免构造函数用浮点数表示,上面已经说过了浮点数本身存在精度丢失的问题。BigDecimal是怎么解决精度丢失的呢?

BigDecimal是通过一个"无标度值"和一个"标度"来表示一个数的。

关键得三个参数:

无标度值(Unscaled Value):这是一个整数,表示BigDecimal的实际数值。

标度(Scale):这是一个整数,表示小数点后的位数。

BigDecimal的实际数值计算公式为:unscaledValue × 10^(-scale)。

假设有一个BigDecimal表示的数值是123.45,那么无标度值(Unscaled Value)是12345。标度(Scale)是2。因为123.45 = 12345 × 10^(-2)。

当标度为正数时,它表示小数点后的位数。例如,在数字123.45中,他的无标度值为12345,标度是2。

当标度为零时,BigDecimal表示一个整数。

当标度为负数时,它表示小数点向左移动的位数,相当于将数字乘以 10 的绝对值的次方。

例如,一个数值为1234500,那么他可以用value是12345,scale为-2来表示,因为1234500 * 10^(-2) = 12345。

(当需要处理非常大的整数时,可以使用负数的标度来指定小数点左侧的位数。这在需要保持整数的精度而又不想丢失尾部零位时很有用。)

不能用BigDecimal的equals方法做等值

直接上源码吧:

kotlin 复制代码
public boolean equals(Object x) {
    if (!(x instanceof BigDecimal)) {
        return false;
    } else {
        BigDecimal xDec = (BigDecimal)x;
        if (x == this) {
            return true;
            //比较标度
        } else if (this.scale != xDec.scale) {
            return false;
        } else {
            long s = this.intCompact;
            long xs = xDec.intCompact;
            if (s != -9223372036854775808L) {
                if (xs == -9223372036854775808L) {
                    xs = compactValFor(xDec.intVal);
                }

                return xs == s;
            } else if (xs != -9223372036854775808L) {
                return xs == compactValFor(this.intVal);
            } else {
                return this.inflated().equals(xDec.inflated());
            }
        }
    }
}

equals方法会比较标度,所以比较大小的话用 compareTo()比较吧

java 复制代码
import java.math.BigDecimal;

public class BigDecimalComparison {
    public static void main(String[] args) {
        BigDecimal num1 = new BigDecimal("1.0");
        BigDecimal num2 = new BigDecimal("1.00");
        System.out.println("equals: " + num1.equlse(num2));       // 输出false
        System.out.println("compareTo: " + num1.compareTo(num2)); // 输出: 0
    }
}

小结

因为某些十进制的小数在二进制中是一个无限循环的小数,所有用浮点数存在进度丢失问题。 BigDecimal采用标度的形式解决了精度丢失问题。同时在使用BigDecimal的时候,使用String的构造器。 最后在使用BigDecimal比较大小的时候不要使用equals,请用compareTo

相关推荐
向前看-2 小时前
验证码机制
前端·后端
xlsw_2 小时前
java全栈day20--Web后端实战(Mybatis基础2)
java·开发语言·mybatis
神仙别闹2 小时前
基于java的改良版超级玛丽小游戏
java
黄油饼卷咖喱鸡就味增汤拌孜然羊肉炒饭3 小时前
SpringBoot如何实现缓存预热?
java·spring boot·spring·缓存·程序员
暮湫3 小时前
泛型(2)
java
超爱吃士力架3 小时前
邀请逻辑
java·linux·后端
南宫生3 小时前
力扣-图论-17【算法学习day.67】
java·学习·算法·leetcode·图论
转码的小石3 小时前
12/21java基础
java
李小白664 小时前
Spring MVC(上)
java·spring·mvc
GoodStudyAndDayDayUp4 小时前
IDEA能够从mapper跳转到xml的插件
xml·java·intellij-idea