经理突然问我为什么BigDecimal可以不丢失精度?我表示...😨

🏆本文收录于「滚雪球学SpringBoot」(全网一个名)专栏,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!

🌟 前言:开发与精度的爱恨情仇

哈咯啊,同学们!今天我要分享一个比较有趣的知识点,恐怕大家基本都回答的不够全面,这也是前天我经理突然对我灵魂来上的重重一击,还好我底子厚,经得住考验,如果换做是你,你能如何满分回答领导提出的拷问?

或许你们曾经也有过因为浮点数的精度问题抓狂?比如说,当你愉快地写下 0.1 + 0.2 时,满心期待的结果为 0.3,结果程序却冷漠地给你输出 0.30000000000000004,直接给你的程序人生当头一棒!😤,这答案连我不懂算数的太奶看了都能直接给晕过去!

类似的问题在计算机中屡见不鲜,尤其是在金融计算、科学计算等领域,如果计算结果稍有偏差,可能就会引发无法挽回的后果。于是,在Java 语言 中,噔噔噔 --- BigDecimal,由此横空出世,它不但精准得让人放心,还能让你远离那些"不靠谱"的浮点数。

今天这篇文章,bug菌我的目的就是带着大家能够全面了解 BigDecimal 的魔法 !既有专业分析,也有生动案例,帮你一文搞懂:为什么 BigDecimal 可以不丢失精度?并且如何用好它?它到底有多强大?,让你秒上手,就是我今天写此文的最终目的,而且让你无论过去多久,依旧能够回忆起,它 - BigDecimal 为什么不会丢失精度?

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

想必这个问题,浮点数的精度问题是每个开发者都无法绕过的坑。在搞懂 BigDecimal 之前,我们必须先弄清楚一个问题:浮点数到底为什么会丢失精度?

💻 浮点数的存储原理

在计算机中,浮点数(floatdouble)的存储基于 IEEE 754 标准。它将一个数字拆分成三个部分存储:

  1. 符号位:表示正负。
  2. 指数:控制数值的范围。
  3. 尾数:保存有效数字。

用通俗点的话来说,浮点数在计算机中是用 二进制科学计数法 表示的,比如:

json 复制代码
0.1 = 1.10011001100110011...(无限循环) × 2^-4

但是,由于计算机存储空间有限,无法表示无限循环小数,因此只能取一个近似值。这就导致:

json 复制代码
0.1 在计算机中实际存储的并不是精确的 0.1,而是一个接近 0.1 的值。

当你进行类似 0.1 + 0.2 的操作时,这些近似值累积起来,就会产生微小误差,比如:

json 复制代码
0.1 + 0.2 = 0.30000000000000004

🎩 BigDecimal 的魔法:为什么它能拯救我们?

BigDecimal 的核心优势在于它使用 字符串存储数字 ,而不是像浮点数那样用二进制表示。它的设计使得它可以避免浮点数的精度丢失问题。那么它到底是怎么做到的?让我们通过 BigDecimal 的源码 来一探究竟!🚀

🔍 从构造方法看 BigDecimal 的存储机制

📜 构造方法源码

以下是 BigDecimal 的部分构造方法的源码:

java 复制代码
// 基于字符串的构造方法
public BigDecimal(String val) {
    // 校验输入的字符串是否为有效数字
    this(new BigDecimalParser(val).bigInteger, BigDecimalParser.scale, 0);
}

// 基于 double 的构造方法
public BigDecimal(double val) {
    this(Double.toString(val));
}

从源码中可以看出,BigDecimal 最推荐的构造方式是通过字符串 ,因为字符串可以确保输入的数值精确无误。相比之下,如果你用 double 初始化 BigDecimal,其实底层仍然会把 double 转换为字符串(通过 Double.toString()),再进一步存储。

这就是为什么我们总是强调:初始化 BigDecimal 时,最好用字符串而不是浮点数,因为直接传递浮点数容易把精度问题带入 BigDecimal。

🔧 BigDecimal 的内部存储结构

深入到 BigDecimal 的底层,我们会发现它的核心存储结构主要有两个部分:

  1. BigInteger:存储整数部分。
  2. int scale:存储小数点后的位数(也就是精度)。

下面是 BigDecimal 的重要字段源码:

java 复制代码
// BigDecimal 核心字段
private final BigInteger intVal; // 用于存储大整数的核心字段
private final int scale;        // 精度:小数点后位数
private transient int precision; // 用于缓存精度的字段

具体来看:

  • intVal 使用 BigInteger 来存储数字,意味着它可以支持任意大小的整数。
  • scale 用来表示小数点后精确的位数。例如 new BigDecimal("1.23")scale 为 2。
  • precision 是一个临时缓存字段,用来加快计算速度。

🎯 示例分析

java 复制代码
BigDecimal bigDecimal = new BigDecimal("123.456");
System.out.println(bigDecimal.unscaledValue()); // 输出:123456
System.out.println(bigDecimal.scale());        // 输出:3

在这个例子中:

  • intVal 实际存储的是 123456,即原数字去掉小数点后的整数值。
  • scale 表示小数点后有 3 位。

示例运行结果展示如下:

通过这种设计,BigDecimal 能够准确地存储任意大小和任意精度的数字,而不会出现浮点数的近似值问题。

🧮 源码示例:加法运算

BigDecimal加法源码展示

以下是 BigDecimal 加法的核心源码:

java 复制代码
public BigDecimal add(BigDecimal augend) {
    // 如果两个数的 scale 相同,直接相加
    if (scale == augend.scale) {
        return new BigDecimal(intVal.add(augend.intVal), scale);
    }
    // 如果 scale 不同,调整 scale 后再相加
    BigInteger scaledIntVal = this.intVal.multiply(BigInteger.TEN.pow(augend.scale - this.scale));
    BigInteger result = scaledIntVal.add(augend.intVal);
    return new BigDecimal(result, Math.max(this.scale, augend.scale));
}

源码解析

如下是对上的完整源码解析,希望能够帮到大家加深对BigDecimal 理解。

java 复制代码
public BigDecimal add(BigDecimal augend) {
    // 如果两个数的 scale 相同,直接相加
    if (scale == augend.scale) {
        return new BigDecimal(intVal.add(augend.intVal), scale);
    }
    // 如果 scale 不同,调整 scale 后再相加
    BigInteger scaledIntVal = this.intVal.multiply(BigInteger.TEN.pow(augend.scale - this.scale));
    BigInteger result = scaledIntVal.add(augend.intVal);
    return new BigDecimal(result, Math.max(this.scale, augend.scale));
}
1. 方法签名
  • 方法名add
    这是 BigDecimal 类的一个方法,用于实现两个 BigDecimal 对象的加法运算。
  • 参数BigDecimal augend
    这是加法运算中的被加数(augend)。BigDecimal 类型的参数表示参与加法运算的另一个数。
  • 返回值BigDecimal
    返回一个新的 BigDecimal 对象,表示两个数的和。
2. 核心逻辑
2.1 检查小数点位数(Scale)
java 复制代码
if (scale == augend.scale) {
    return new BigDecimal(intVal.add(augend.intVal), scale);
}
  • scaleBigDecimal 中的 scale 表示小数点后的位数。例如,123.45scale 是 2,123.456scale 是 3。
  • 逻辑 :如果两个 BigDecimal 对象的小数点位数(scale)相同,可以直接对它们的整数部分(intVal)进行加法运算。
    • intValBigDecimal 内部使用 BigInteger 来存储数值的整数部分。intVal 表示去掉小数点后的整数部分。
    • 操作 :调用 intVal.add(augend.intVal),将两个 BigInteger 直接相加。
    • 返回结果 :构造一个新的 BigDecimal 对象,其整数部分是加法结果,小数点位数(scale)保持不变。

示例

假设 this = 123.45augend = 678.90,它们的 scale 都是 2。

  • intVal 分别是 1234567890
  • 直接相加:12345 + 67890 = 80235
  • 返回结果:new BigDecimal(80235, 2),即 802.35
2.2 处理不同小数点位数(Scale)
java 复制代码
BigInteger scaledIntVal = this.intVal.multiply(BigInteger.TEN.pow(augend.scale - this.scale));
BigInteger result = scaledIntVal.add(augend.intVal);
return new BigDecimal(result, Math.max(this.scale, augend.scale));
  • 逻辑 :如果两个 BigDecimal 对象的小数点位数(scale)不同,需要先将它们调整到相同的小数点位数,再进行加法运算。
    1. 调整小数点位数
      • augend.scale - this.scale:计算两个数的小数点位数差。
      • BigInteger.TEN.pow(augend.scale - this.scale) :生成一个 10 的幂次方,用于调整小数点位数。例如,如果 augend.scale - this.scale = 2,则生成 100
      • this.intVal.multiply(...) :将当前对象的整数部分乘以 10 的幂次方,调整小数点位数,使其与被加数的 scale 一致。
    2. 加法运算
      • scaledIntVal.add(augend.intVal):将调整后的整数部分与被加数的整数部分相加。
    3. 构造结果
      • Math.max(this.scale, augend.scale) :选择较大的 scale 作为结果的小数点位数。
      • new BigDecimal(result, ...) :构造一个新的 BigDecimal 对象,其整数部分是加法结果,小数点位数为较大的 scale

示例

假设 this = 123.45scale = 2)和 augend = 67.890scale = 3)。

  • intVal 分别是 1234567890
  • 调整小数点位数:
    • augend.scale - this.scale = 3 - 2 = 1
    • 10.pow(1) = 10
    • this.intVal.multiply(10) = 12345 * 10 = 123450
  • 加法运算:
    • 123450 + 67890 = 191340
  • 构造结果:
    • Math.max(this.scale, augend.scale) = Math.max(2, 3) = 3
    • 返回结果:new BigDecimal(191340, 3),即 191.340
3. 小结

如上源码的核心逻辑是实现两个 BigDecimal 对象的加法运算。它通过以下步骤确保加法的正确性:

  1. 检查小数点位数(scale
    • 如果两个数的 scale 相同,直接对整数部分进行加法运算。
  2. 调整小数点位数
    • 如果 scale 不同,将较小 scale 的数调整到较大的 scale,再进行加法运算。
  3. 构造结果
    • 使用较大的 scale 构造新的 BigDecimal 对象。

这种方法确保了加法运算的精确性,同时避免了因小数点位数不同导致的计算错误。

4. 设计意图
  • 精确性BigDecimal 用于处理高精度的数值运算,特别是在金融和科学计算中。通过精确调整小数点位数,确保加法运算的结果精确无误。
  • 效率 :在 scale 相同时直接相加,避免了不必要的调整操作,提高了性能。
  • 通用性:能够处理任意小数点位数的加法运算,适用于各种场景。

希望这段解析对同学们理解 BigDecimal.add 方法有帮助!如果还有其他问题,欢迎继续提问。

🎯 示例分析

java 复制代码
BigDecimal num1 = new BigDecimal("1.23");
BigDecimal num2 = new BigDecimal("4.567");
BigDecimal result = num1.add(num2);
System.out.println(result); // 输出:5.797

示例运行结果展示如下:

🛠️ BigDecimal 的用法全面讲解

接下来,我们通过一个个具体案例来看看如何正确使用 BigDecimal。快来打开你的 IDE,跟着我一起代码实战一起学吧!

🎬 基本操作:加减乘除

加减乘除案例代码

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

/**
 * @author bug菌
 * @Source 公众号:猿圈奇妙屋
 */
public class OdMain {

    public static void main(String[] args) {
        BigDecimal num1 = new BigDecimal("0.1");
        BigDecimal num2 = new BigDecimal("0.2");

        // 加法
        BigDecimal sum = num1.add(num2);
        System.out.println("加法结果:" + sum); // 输出:0.3

        // 减法
        BigDecimal diff = num1.subtract(num2);
        System.out.println("减法结果:" + diff); // 输出:-0.1

        // 乘法
        BigDecimal product = num1.multiply(num2);
        System.out.println("乘法结果:" + product); // 输出:0.02

        // 除法
        BigDecimal quotient = num1.divide(num2, 2, RoundingMode.HALF_UP);
        System.out.println("除法结果:" + quotient); // 输出:0.50
    }
}

案例执行结果

根据如上的测试用例,作者在本地进行测试结果如下,仅供参考,你们也可以自行修改测试用例或者添加其他的测试数据或测试方法,以便于进行熟练学习以此加深知识点的理解。实际运行结果展示如下:

用例代码分析

在本次的代码演示中,我将会深入剖析每句代码,详细阐述其背后的设计思想和实现逻辑。通过这样的讲解方式,我希望能够引导同学们逐步构建起对代码的深刻理解。我会先从代码的结构开始,逐步拆解每个模块的功能和作用,并指出关键的代码段,并解释它们是如何协同运行的。通过这样的讲解和实践相结合的方式,我相信每位同学都能够对代码有更深入的理解,并能够早日将其掌握,应用到自己的学习和工作中。

如上这段代码演示了 BigDecimal 类在Java中进行高精度算术运算的能力,包括加法、减法、乘法和除法。BigDecimal 是一个用于处理精确浮点数运算的类,特别适用于需要高精度计算的场景,如金融和科学计算。

1. 创建 BigDecimal 对象

程序首先创建了两个 BigDecimal 对象:

  • num1 表示数值 0.1
  • num2 表示数值 0.2

这里使用字符串构造 BigDecimal 对象的原因是为了避免浮点数表示不准确的问题。例如,直接使用 new BigDecimal(0.1) 可能会导致精度问题,因为 0.1 在浮点数表示中是不精确的。而使用字符串构造可以确保数值的精确表示。

2. 加法运算

程序执行了加法运算:

  • num1num2 相加,即 0.1 + 0.2
  • 结果是 0.3,并打印出来。

BigDecimaladd 方法用于执行加法运算,它会精确地计算两个数的和,避免了浮点数运算中的精度问题。

3. 减法运算

程序执行了减法运算:

  • num1 减去 num2,即 0.1 - 0.2
  • 结果是 -0.1,并打印出来。

BigDecimalsubtract 方法用于执行减法运算,同样确保了结果的精确性。

4. 乘法运算

程序执行了乘法运算:

  • num1 乘以 num2,即 0.1 * 0.2
  • 结果是 0.02,并打印出来。

BigDecimalmultiply 方法用于执行乘法运算,确保了乘法结果的精确性。

5. 除法运算

程序执行了除法运算:

  • num1 除以 num2,即 0.1 / 0.2
  • 结果保留两位小数,并使用四舍五入的舍入模式。
  • 最终结果是 0.50,并打印出来。

BigDecimaldivide 方法用于执行除法运算。它需要指定结果的小数点位数(scale)和舍入模式(RoundingMode)。在这个例子中,结果保留两位小数,并使用 RoundingMode.HALF_UP(四舍五入)作为舍入模式。

关键点

  1. 精度问题

    • BigDecimal 用于处理高精度的浮点数运算,避免了 floatdouble 类型的精度问题。
    • 使用字符串构造 BigDecimal 对象可以确保数值的精确表示。
  2. 舍入模式

    • 在除法运算中,需要指定舍入模式,因为除法可能会产生无限循环的小数。
    • RoundingMode.HALF_UP 表示四舍五入,是常用的舍入模式之一。
  3. 方法功能

    • add:执行加法运算。
    • subtract:执行减法运算。
    • multiply:执行乘法运算。
    • divide:执行除法运算,需要指定结果的小数点位数和舍入模式。

小结

如上代码通过 BigDecimal 类展示了如何在Java中进行高精度的算术运算。它涵盖了加法、减法、乘法和除法的基本操作,并通过指定舍入模式确保了除法运算的精确性。程序逻辑清晰,适合初学者学习 BigDecimal 的基本用法。

🧮 舍入模式

BigDecimal 提供了 8 种舍入模式,以下是常用的几个:

  1. RoundingMode.HALF_UP:四舍五入。
  2. RoundingMode.HALF_DOWN:五舍六入。
  3. RoundingMode.CEILING:向上舍入。
  4. RoundingMode.FLOOR:向下舍入。
java 复制代码
BigDecimal number = new BigDecimal("10.125");
BigDecimal rounded = number.setScale(2, RoundingMode.HALF_UP);
System.out.println("四舍五入结果:" + rounded); // 输出:10.13

根据如上的测试用例,作者在本地进行测试结果如下,仅供参考,你们也可以自行修改测试用例或者添加其他的测试数据或测试方法,以便于进行熟练学习以此加深知识点的理解。

📊 比较大小

java 复制代码
BigDecimal a = new BigDecimal("1.23");
BigDecimal b = new BigDecimal("4.56");

int result = a.compareTo(b);
System.out.println(result); // 输出:-1 表示 a < b

根据如上的测试用例,作者在本地进行测试结果如下,仅供参考,你们也可以自行修改测试用例或者添加其他的测试数据或测试方法,以便于进行熟练学习以此加深知识点的理解。

💡 实战演示:BigDecimal 在日常开发中的妙用

🎯 场景 1:货币计算

货币计算是 BigDecimal 最经典的应用场景,比如计算商品价格总额:

java 复制代码
BigDecimal price = new BigDecimal("19.99");
BigDecimal quantity = new BigDecimal("3");
BigDecimal total = price.multiply(quantity).setScale(2, RoundingMode.HALF_UP);
System.out.println("总价:" + total); // 输出:59.97

如下是实际案例运行结果展示:

🎯 场景 2:科学计算

科学计算中,我们需要对精度要求极高的数值进行运算:

java 复制代码
BigDecimal base = new BigDecimal("2");
BigDecimal exponent = new BigDecimal("10");
BigDecimal result = base.pow(exponent.intValue());
System.out.println("2 的 10 次方:" + result); // 输出:1024

如下是实际案例运行结果展示:

🎯 场景 3:统计分析

在统计数据时,BigDecimal 可以避免累计误差:

java 复制代码
BigDecimal[] numbers = {
    new BigDecimal("1.23"),
    new BigDecimal("4.56"),
    new BigDecimal("7.89")
};

BigDecimal sum = BigDecimal.ZERO;
for (BigDecimal num : numbers) {
    sum = sum.add(num);
}
System.out.println("总和:" + sum); // 输出:13.68

如下是实际案例运行结果展示:

🤓 进阶拓展:BigDecimal 的高级用法

🔄 转换与输出

  1. 转字符串
java 复制代码
   BigDecimal num = new BigDecimal("123.45");
   String str = num.toString();
   System.out.println(str); // 输出:123.45
  1. 转双精度
java 复制代码
   double val = num.doubleValue();
   System.out.println(val); // 输出:123.45

🧑‍🏭 自定义运算

java 复制代码
BigDecimal num1 = new BigDecimal("10");
BigDecimal num2 = new BigDecimal("3");
BigDecimal result = num1.divide(num2, 5, RoundingMode.HALF_UP);
System.out.println("商:" + result); // 输出:3.33333

🎉 总结:为什么精度问题的解决之道是 BigDecimal?

总而言之,BigDecimal之所以能够解决精度问题,归纳有仨:

精确表示

  • BigDecimal 使用十进制表示法,能够精确表示任意精度的浮点数,避免了二进制表示带来的精度问题。
  • 例如,0.1BigDecimal 中可以精确表示为 0.1,而不是近似值。

精确运算

  • BigDecimal 的算术运算方法(如 addsubtractmultiplydivide)在执行时会考虑小数点位置(scale),确保结果的精确性。
  • 例如,0.1 + 0.2 的结果是 0.3,而不是 0.30000000000000004

可配置的舍入模式

  • 在需要舍入的情况下,BigDecimal 提供了多种舍入模式,允许用户根据具体需求选择合适的舍入方式。
  • 例如,在金融计算中,通常使用 RoundingMode.HALF_UP(四舍五入)来确保结果的精确性。

简言之,BigDecimal 能够解决浮点数精度问题,原因在于其内部使用十进制表示法,能够精确表示任意精度的浮点数,并提供精确的算术运算方法。与 floatdouble 类型相比,BigDecimal 避免了二进制表示带来的精度问题,适用于需要高精度计算的场景。   虽然它在性能和操作简便性上稍逊于 double,但在需要精度的地方,它绝对是你的得力助手。记住:当你需要靠谱的计算结果时,不要犹豫,直接选择 BigDecimal 吧!🎯

📣 关于我

我是bug菌,CSDN | 掘金 | InfoQ | 51CTO | 华为云 | 阿里云 | 腾讯云 等社区博客专家,C站博客之星Top30,华为云多年度十佳博主&最具价值贡献奖,掘金多年度人气作者Top40,掘金等各大社区平台签约作者,51CTO年度博主Top12,掘金/InfoQ/51CTO等社区优质创作者;全网粉丝合计 30w+ ;硬核微信公众号「猿圈奇妙屋」,欢迎你的加入!免费白嫖最新BAT互联网公司面试真题、4000G PDF电子书籍、简历模板等海量资料,你想要的我都有,关键是你不来拿。

-End-

相关推荐
Python私教1 分钟前
Java手写链表全攻略:从单链表到双向链表的底层实现艺术
java·python·链表
吴生439610 分钟前
数据库ALGORITHM = INSTANT 特性研究过程
后端
小麟有点小靈14 分钟前
VSCode写java时常用的快捷键
java·vscode·编辑器
程序猿chen25 分钟前
JVM考古现场(十九):量子封神·用鸿蒙编译器重铸天道法则
java·jvm·git·后端·程序人生·java-ee·restful
Chandler2442 分钟前
Go:接口
开发语言·后端·golang
&白帝&44 分钟前
java HttpServletRequest 和 HttpServletResponse
java·开发语言
ErizJ1 小时前
Golang|Channel 相关用法理解
开发语言·后端·golang
automan021 小时前
golang 在windows 系统的交叉编译
开发语言·后端·golang
Pandaconda1 小时前
【新人系列】Golang 入门(十三):结构体 - 下
后端·golang·go·方法·结构体·后端开发·值传递
我是谁的程序员1 小时前
Flutter iOS真机调试报错弹窗:不受信任的开发者
后端