浮点数为什么会丢失精度?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

相关推荐
松仔log17 分钟前
JetPack——Paging3+Room
android·java·zoom
㳺三才人子5 小时前
初探 Flask
后端·python·flask·html
星栈独行5 小时前
我在 Rust 全栈项目里用 JWT 做无状态认证
开发语言·后端·rust·前端框架·开源·github·web
Lei活在当下6 小时前
先用起来,再理解,关于协程Coroutine应该知道的事
android·java·jvm
Java爱好狂.6 小时前
Java程序员体系化学习路线(2026最新版)
java·后端·java面试·java架构师·java程序员·java八股文·java学习路线
陈随易6 小时前
Redis 8.8发布,一定要更新
前端·后端·程序员
tongluowan0076 小时前
以ReentrantLock为例解释AQS的工作流程
java·模板方法模式·aqs·reentrantlock
装不满的克莱因瓶6 小时前
SpringBoot 如何将 lib 目录中jar包打包进最终的jar包里面
spring boot·后端·maven·jar·mvn
ltl7 小时前
Transformer 原论文实验结果:为什么 28.4 BLEU 足以改写路线图
后端
身如柳絮随风扬7 小时前
Java 项目打包与部署完全指南:JAR vs WAR,从构建到运行
java·firefox·jar