目录

  • 问题现象
  • 根本原因
  • 详细分析
  • 实际验证
  • 解决方案
  • 最佳实践
  • 总结

一开始看到这个说法的时候我还不相信,还以为之前我学的都错完了,研究之后才明白为什么

问题现象

令人困惑的计算结果

public class FloatPrecisionDemo {public static void main(String[] args) {// 这些看似简单的计算,结果却出人意料System.out.println("0.05 + 0.01 = " + (0.05 + 0.01));System.out.println("0.1 + 0.2 = " + (0.1 + 0.2));System.out.println("1.0 - 0.9 = " + (1.0 - 0.9));System.out.println("0.3 * 3 = " + (0.3 * 3));}
}

运行结果

0.05 + 0.01 = 0.060000000000000005
0.1 + 0.2 = 0.30000000000000004
1.0 - 0.9 = 0.09999999999999998
0.3 * 3 = 0.8999999999999999

等等!这些结果明显不对! 0.05 + 0.01 应该等于 0.06,为什么会出现这么多小数位?

根本原因

1. 二进制表示的局限性

十进制转二进制的精度问题
0.05 (十进制) = 0.0001100110011001100110011001100110011001100110011001101... (二进制)
0.01 (十进制) = 0.00000010100011110101110000101000111101011100001010001111... (二进制)

关键问题:这些小数在二进制中是无限循环小数,无法精确表示!

为什么会出现无限循环?
// 以0.05为例,转换为二进制的过程:
// 0.05 × 2 = 0.1  → 整数部分0,小数部分0.1
// 0.1 × 2 = 0.2   → 整数部分0,小数部分0.2
// 0.2 × 2 = 0.4   → 整数部分0,小数部分0.4
// 0.4 × 2 = 0.8   → 整数部分0,小数部分0.8
// 0.8 × 2 = 1.6   → 整数部分1,小数部分0.6
// 0.6 × 2 = 1.2   → 整数部分1,小数部分0.2
// 0.2 × 2 = 0.4   → 循环开始...// 结果:0.05 = 0.0001100110011001100110011001100110011001100110011001101...

2. IEEE 754浮点数标准

double类型的存储格式
64位 = 1位符号位 + 11位指数位 + 52位尾数位
精度限制
// double类型只能精确表示有限位数
// 对于0.05和0.01,在二进制中都是无限循环小数
// 存储时会被截断,导致精度丢失// 0.05在double中的实际存储值:
// 0.05000000000000000277555756156289135105907917022705078125// 0.01在double中的实际存储值:
// 0.01000000000000000020816681711721685132943093776702880859375

详细分析

1. 0.05的二进制表示

// 0.05的二进制表示(无限循环)
// 0.0001100110011001100110011001100110011001100110011001101...// 转换为科学计数法
// 1.1001100110011001100110011001100110011001100110011010 × 2^(-5)// 在double中存储时,52位尾数无法完全表示这个无限循环
// 导致精度丢失

2. 0.01的二进制表示

// 0.01的二进制表示(无限循环)
// 0.00000010100011110101110000101000111101011100001010001111...// 转换为科学计数法
// 1.0100011110101110000101000111101011100001010001111011 × 2^(-7)// 同样存在精度丢失

3. 相加过程

// 0.05 + 0.01 的实际计算过程
// 由于精度丢失,实际计算的是近似值
// 结果:0.060000000000000005// 详细过程:
// 0.05 (存储值) = 0.05000000000000000277555756156289135105907917022705078125
// 0.01 (存储值) = 0.01000000000000000020816681711721685132943093776702880859375
// 相加结果 = 0.060000000000000004991640087923096656986236572265625

实际验证

1. 更多浮点数精度问题

public class FloatPrecisionTest {public static void main(String[] args) {System.out.println("=== 浮点数精度问题演示 ===");// 基本运算System.out.println("0.1 + 0.2 = " + (0.1 + 0.2));System.out.println("0.05 + 0.01 = " + (0.05 + 0.01));System.out.println("1.0 - 0.9 = " + (1.0 - 0.9));System.out.println("0.3 * 3 = " + (0.3 * 3));// 比较问题double a = 0.1 + 0.2;double b = 0.3;System.out.println("0.1 + 0.2 == 0.3: " + (a == b));// 累积误差double sum = 0.0;for (int i = 0; i < 10; i++) {sum += 0.1;}System.out.println("0.1累加10次 = " + sum);System.out.println("sum == 1.0: " + (sum == 1.0));}
}

运行结果

=== 浮点数精度问题演示 ===
0.1 + 0.2 = 0.30000000000000004
0.05 + 0.01 = 0.060000000000000005
1.0 - 0.9 = 0.09999999999999998
0.3 * 3 = 0.8999999999999999
0.1 + 0.2 == 0.3: false
0.1累加10次 = 0.9999999999999999
sum == 1.0: false

2. 使用BigDecimal解决

import java.math.BigDecimal;
import java.math.RoundingMode;public class BigDecimalSolution {public static void main(String[] args) {System.out.println("=== BigDecimal精确计算 ===");// 正确的BigDecimal使用方式BigDecimal a = new BigDecimal("0.05");BigDecimal b = new BigDecimal("0.01");BigDecimal result = a.add(b);System.out.println("BigDecimal: 0.05 + 0.01 = " + result);BigDecimal c = new BigDecimal("0.1");BigDecimal d = new BigDecimal("0.2");System.out.println("BigDecimal: 0.1 + 0.2 = " + c.add(d));// 错误的BigDecimal使用方式BigDecimal wrong1 = new BigDecimal(0.05);BigDecimal wrong2 = new BigDecimal(0.01);BigDecimal wrongResult = wrong1.add(wrong2);System.out.println("错误方式: " + wrongResult);}
}

运行结果

=== BigDecimal精确计算 ===
BigDecimal: 0.05 + 0.01 = 0.06
BigDecimal: 0.1 + 0.2 = 0.3
错误方式: 0.060000000000000004991640087923096656986236572265625

解决方案

1. 使用BigDecimal(推荐)

正确的构造方式
// ✅ 正确方式:使用字符串构造器
BigDecimal correct1 = new BigDecimal("0.05");
BigDecimal correct2 = new BigDecimal("0.01");// ✅ 正确方式:使用valueOf方法
BigDecimal alsoCorrect = BigDecimal.valueOf(0.1);// ❌ 错误方式:使用double构造器
BigDecimal wrong = new BigDecimal(0.05);  // 会保留精度误差
完整的BigDecimal示例
import java.math.BigDecimal;
import java.math.RoundingMode;public class FinancialCalculator {public BigDecimal calculateInterest(BigDecimal principal, BigDecimal rate) {// 精确的利息计算return principal.multiply(rate).setScale(2, RoundingMode.HALF_UP);}public BigDecimal calculateTotal(BigDecimal price, int quantity) {// 精确的价格计算return price.multiply(new BigDecimal(quantity)).setScale(2, RoundingMode.HALF_UP);}public BigDecimal calculateDiscount(BigDecimal originalPrice, BigDecimal discountRate) {// 精确的折扣计算BigDecimal discount = originalPrice.multiply(discountRate);return originalPrice.subtract(discount).setScale(2, RoundingMode.HALF_UP);}
}

2. 浮点数比较的正确方式

public class FloatComparison {public static void main(String[] args) {double a = 0.1 + 0.2;double b = 0.3;//  错误的比较方式System.out.println("a == b: " + (a == b));  // false//  正确的比较方式:使用误差范围double epsilon = 1e-10;System.out.println("|a - b| < epsilon: " + (Math.abs(a - b) < epsilon));  // true//  正确的比较方式:使用BigDecimalBigDecimal bd1 = new BigDecimal("0.1").add(new BigDecimal("0.2"));BigDecimal bd2 = new BigDecimal("0.3");System.out.println("bd1.equals(bd2): " + bd1.equals(bd2));  // true}
}

3. 工具类封装

public class PrecisionUtils {private static final double EPSILON = 1e-10;/*** 比较两个浮点数是否相等*/public static boolean isEqual(double a, double b) {return Math.abs(a - b) < EPSILON;}/*** 比较两个浮点数是否相等(自定义误差范围)*/public static boolean isEqual(double a, double b, double epsilon) {return Math.abs(a - b) < epsilon;}/*** 舍入浮点数到指定小数位*/public static double round(double value, int places) {double scale = Math.pow(10, places);return Math.round(value * scale) / scale;}/*** 安全的浮点数加法*/public static BigDecimal safeAdd(double a, double b) {return new BigDecimal(String.valueOf(a)).add(new BigDecimal(String.valueOf(b)));}
}

最佳实践

1. 选择合适的数据类型

场景推荐类型原因
金融计算BigDecimal需要精确计算
科学计算double性能好,精度足够
整数计算int/long精确,性能最好
一般计算double平衡性能和精度

2. BigDecimal使用规范

public class BigDecimalBestPractices {//  正确的使用方式public BigDecimal calculatePrice(BigDecimal unitPrice, int quantity) {return unitPrice.multiply(new BigDecimal(quantity)).setScale(2, RoundingMode.HALF_UP);}//  使用常量避免重复创建private static final BigDecimal HUNDRED = new BigDecimal("100");private static final BigDecimal ZERO = BigDecimal.ZERO;public BigDecimal calculatePercentage(BigDecimal value, BigDecimal total) {if (total.equals(ZERO)) {return ZERO;}return value.multiply(HUNDRED).divide(total, 2, RoundingMode.HALF_UP);}//  处理除零异常public BigDecimal safeDivide(BigDecimal numerator, BigDecimal denominator) {if (denominator.equals(ZERO)) {throw new ArithmeticException("除数不能为零");}return numerator.divide(denominator, 10, RoundingMode.HALF_UP);}
}

3. 性能优化建议

public class PerformanceOptimization {// 对于不需要高精度的场景,使用基本类型public double calculateTemperature(double celsius) {return celsius * 9.0 / 5.0 + 32.0;  // 温度转换,精度要求不高}// 对于需要高精度的场景,使用BigDecimalpublic BigDecimal calculateInterest(BigDecimal principal, BigDecimal rate) {return principal.multiply(rate).setScale(2, RoundingMode.HALF_UP);}// 缓存常用的BigDecimal值private static final Map<String, BigDecimal> CACHE = new HashMap<>();public BigDecimal getCachedValue(String key) {return CACHE.computeIfAbsent(key, BigDecimal::new);}
}

常见陷阱

1. BigDecimal构造陷阱

//  陷阱1:使用double构造器
BigDecimal trap1 = new BigDecimal(0.1);  // 0.1000000000000000055511151231257827021181583404541015625//  陷阱2:使用float构造器
BigDecimal trap2 = new BigDecimal(0.1f);  // 0.100000001490116119384765625//  正确方式:使用字符串构造器
BigDecimal correct = new BigDecimal("0.1");  // 0.1//  正确方式:使用valueOf方法
BigDecimal alsoCorrect = BigDecimal.valueOf(0.1);  // 0.1

2. 舍入模式陷阱

BigDecimal number = new BigDecimal("3.14159");// 不同的舍入模式会产生不同结果
System.out.println("HALF_UP: " + number.setScale(2, RoundingMode.HALF_UP));      // 3.14
System.out.println("HALF_DOWN: " + number.setScale(2, RoundingMode.HALF_DOWN));  // 3.14
System.out.println("CEILING: " + number.setScale(2, RoundingMode.CEILING));      // 3.15
System.out.println("FLOOR: " + number.setScale(2, RoundingMode.FLOOR));          // 3.14

3. 比较陷阱

BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("1.00");//  陷阱:equals方法比较值和精度
System.out.println("equals: " + a.equals(b));  // false//  正确方式:compareTo方法只比较值
System.out.println("compareTo: " + (a.compareTo(b) == 0));  // true

性能对比

1. 运算速度对比

public class PerformanceComparison {public static void main(String[] args) {int iterations = 1000000;// double运算long start1 = System.currentTimeMillis();double sum1 = 0.0;for (int i = 0; i < iterations; i++) {sum1 += 0.1;}long time1 = System.currentTimeMillis() - start1;// BigDecimal运算long start2 = System.currentTimeMillis();BigDecimal sum2 = BigDecimal.ZERO;for (int i = 0; i < iterations; i++) {sum2 = sum2.add(new BigDecimal("0.1"));}long time2 = System.currentTimeMillis() - start2;System.out.println("double运算时间: " + time1 + "ms");System.out.println("BigDecimal运算时间: " + time2 + "ms");System.out.println("性能差异: " + (time2 / time1) + "倍");}
}

2. 内存占用对比

类型内存占用精度适用场景
double8字节15-17位有效数字科学计算
BigDecimal可变(通常>20字节)任意精度金融计算

总结

核心要点

  1. 浮点数精度问题的根本原因

    • 某些十进制小数在二进制中是无限循环小数
    • IEEE 754标准只能存储有限位数
    • 存储时精度丢失导致计算误差
  2. 解决方案

    • 金融计算:使用BigDecimal
    • 科学计算:使用double,注意精度问题
    • 比较浮点数:使用误差范围或BigDecimal
  3. 最佳实践

    • 使用字符串构造BigDecimal
    • 选择合适的舍入模式
    • 注意性能权衡

实际应用建议

  • 金融系统:必须使用BigDecimal,确保计算精确
  • 科学计算:可以使用double,但要注意精度问题
  • 一般应用:根据精度要求选择合适的数据类型
  • 性能敏感:权衡精度和性能需求

记住这些关键点

  1. 0.05 + 0.01 ≠ 0.06 是正常的浮点数行为
  2. BigDecimal是金融计算的救星
  3. 字符串构造BigDecimal避免精度丢失
  4. 选择合适的舍入模式很重要
  5. 性能与精度需要权衡

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/diannao/96964.shtml
繁体地址,请注明出处:http://hk.pswp.cn/diannao/96964.shtml
英文地址,请注明出处:http://en.pswp.cn/diannao/96964.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【44页PPT】DeepSeek在银行业务场景的应用(附下载方式)

篇幅所限&#xff0c;本文只提供部分资料内容&#xff0c;完整资料请看下面链接 https://download.csdn.net/download/2501_92808811/91716562 资料解读&#xff1a;【44页PPT】DeepSeek在银行业务场景的应用 详细资料请看本解读文章的最后内容。在智能化时代的浪潮下&#x…

TOPSIS

概述TOPSIS&#xff08;逼近理想解排序法&#xff09;是一种多属性决策方法&#xff0c;通过计算各方案与 “理想解”“负理想解” 的距离&#xff0c;排序选最优。操作步骤输入原始决策矩阵&#xff08;方案 指标&#xff09;&#xff1b;标准化处理&#xff08;消除量纲&…

Base64 编码优化 Web 图片加载:异步响应式架构(Java 后端 + 前端全流程实现)

异步响应式图片加载与Base64编码实现方案 在Web开发中&#xff0c;图片加载效率直接影响页面性能和用户体验。本文介绍一套基于Java后端和JavaScript前端的实现方案&#xff0c;通过Base64编码传输图片&#xff0c;结合异步加载和响应式布局&#xff0c;实现高效、安全的图片展…

【C语言】分支和循环

目录 前置&#xff1a;关系操作符和逻辑操作符 关系操作符 逻辑操作符 其他补充知识 分支语句&#xff1a; 一、if类 基本式&#xff1a;if... 变式1&#xff1a;if...else... 变式2&#xff1a;if...else if...else... 变式3&#xff1a;嵌套 二、switch 循环语句&…

商超客流密度统计误差率↓35%!陌讯多模态融合算法在零售智慧运营的实战解析

原创声明 本文为原创技术解析文章&#xff0c;核心技术参数与架构设计引用自 “陌讯技术白皮书&#xff08;2024 版&#xff09;”&#xff0c;技术描述均经过重写转换&#xff0c;无复制官网文案情况&#xff0c;仅用于计算机视觉技术交流与实战经验分享。 一、零售客流统计的…

游戏空间划分技术

【前言】 空间划分主要是为了降低搜索比较量&#xff0c;如果不采用空间划分&#xff0c;暴力遍历也是可以求解的&#xff0c;但耗时过长。通过空间划分将全局搜索简化为为局部搜索&#xff0c;大大降低搜索量。 搜索出来后最终还要是一一比较&#xff0c;比较的是距离&#…

【C#】观察者模式 + UI 线程调度、委托讲解

“观察者模式 UI 线程调度”的典型应用A. 涉及的知识点&#xff08;抽象&#xff09;观察者模式&#xff08;Observer Pattern&#xff09; 发布者&#xff1a;DemoDeviceService.cs 内部生成一帧数据 ScopeFrame&#xff0c;通过 OnScopeFrame?.Invoke(frame) 发布事件。订阅…

Linux应用软件编程---网络编程(TCP:[ 其他机制、头部标志位、应用示例 ]、 HTTP:[ 万维网、概念、格式、报文、应用示例 ]

一、TCP 网络协议补充内容1、TCP 的其他机制1&#xff09;TCP 头部的标志位TCP 头部可用抓包工具 (wireshark) 来查看。头部标志位用途SYN请求建立连接标志位ACK响应报文标志位PSH携带数据标志位&#xff0c;通知接收方该从缓冲区读数据FIN请求断开连接标志位RST复位标志位URG紧…

基于开源飞控pix的无人机装调与测试

文章目录 前言资源下载1、地面站软件独家汉化版QGC地面站&#xff08;推荐&#xff09;原版QGC地面站Mission Planner地面站 2、安装好环境的虚拟机安装虚拟机打开虚拟机文件 3、完整的各版本PX4、QGC源码PX4QGC 一、无人机基本常识/预备知识&#xff08;1&#xff09;无人机飞…

Ubuntu解决makefile交叉编译的问题

问题1&#xff1a;/usr/lib/gcc-cross/aarch64-linux-gnu/11/../../../../aarch64-linux-gnu/bin/ld: cannot find -lwiringpi: No such file or directory 找不到-lwiringpi库路径&#xff0c;其实在3rd/usr/lib/aarch64-linux-gnu下没有libwiringPi.so.2 …

ExcelUtils实现 设置内容 插入行 复制行列格式

ExcelUtils实现&#xff1a;1.实现输入 例如 2 A 的excel格式&#xff0c;自动填充对应excel单元格&#xff1b;2.实现复制并新增下一行&#xff1b;3.实现控制复制上一行相同列的格式&#xff1b;4.实现控制复制同一行上一列的格式&#xff1b;/*** 在指定行下方插入新行并复…

SQLBot 智能问数、数据洞察逻辑拆解

* 基于 SQLBot v1.0.2* 使用 AI Gateway 抓取模型调用记录SQLBot 通过融入 LLM 能力实现了非常优秀的问数体验&#xff0c;这里记录一下产品中如何引入 AI 能力&#xff0c;顺便探究一下调用大模型的数据安全的问题&#xff08;是否会向模型提供真实数据&#xff09;。结论&…

实现统一门户登录跳转免登录

统一门户所有应用页面&#xff0c;点击跳转对应业务系统&#xff0c;实现业务系统免登录//获取所有业务系统项&#xff08;获取并存储到仓库) //用于页面展示 let appSubjectVoList ref<any>([]) appSubjectVoList.value userStore.getAppSubjectVoList || [] //登陆后…

卓伊凡的开源战略与PHP-SG16加密技术深度解析-sg加密技术详解-卓伊凡

卓伊凡的开源战略与PHP-SG16加密技术深度解析-sg加密技术详解-卓伊凡引言&#xff1a;在理想与现实间寻求平衡的开源之路近日&#xff0c;技术创业者卓伊凡先生宣布了一项重大决策&#xff1a;将于明日将其公司旗下的优雅草商城、项目管理系统等众多成熟商业产品正式开源。这一…

回溯 算法常见面试问题

1. 全排列(无重复元素) 核心思想:交换法避免额外空间 def permute(nums):def backtrack(first=0):if first == len(nums):res.append(nums.copy())returnfor i in range(first, len(nums)):nums[first], nums[i] = nums[i], nums[first]backtrack(first + 1)nums[first], …

营销专业人员核心能力构建与发展路径

CDA数据分析师证书含金量高&#xff0c;适应了未来数字化经济和AI发展趋势&#xff0c;难度不高&#xff0c;行业认可度高&#xff0c;对于找工作很有帮助。一、营销人员五维能力模型能力维度核心技能要素工具与方法论产出成果数据驱动决策指标监控、归因分析、效果优化Google …

Android系统学习2——Android.Utils.Log模块讨论

Android系统学习2——Android.Utils.Log模块讨论 ​ 打日志是一个很好的习惯&#xff0c;有的时候我们可以通过这里排查我们的程序的问题。在这里&#xff0c;我们可以从Android的日志机制入手讨论我们的Log模块。 android.util.Log 类的作用 Android 中最常用的日志工具是 and…

使用 YAML 文件,如何优雅地删除 k8s 资源?

在 Kubernetes 中&#xff0c;删除资源是日常运维中不可避免的操作。如果你习惯了使用 kubectl create 和 kubectl apply 来创建和更新资源&#xff0c;那么你可能也会想知道如何用同样基于文件的方式来删除它们。 虽然你总是可以用 kubectl delete deployment <name> 这…

如何将游戏和软件移动到另一个驱动器或外部磁盘中

您的C盘存储空间是否不足&#xff0c;或者您不小心在错误的驱动器中安装了游戏或应用程序。那么使用这个简单的技巧&#xff0c;您可以轻松的将游戏或应用程序移动到另一个分区或磁盘中。1、找到准备移动的软件&#xff0c;选择路径并复制&#xff1a;2、打开记事本&#xff0c…

赋能汽车电子智造:全星QMS打造品质检验、稽核与客诉管理闭环​——全星质量管理软件系统

全星QMS&#xff1a;驱动汽车电子质量卓越与商业成功的核心引擎 在智能汽车时代&#xff0c;汽车电子的质量已成为产品安全、性能与品牌信誉的核心。面对复杂的供应链、严苛的IATF 16949/ISO 26262标准及降本增效的压力&#xff0c;您的企业需要一位数字化战略伙伴。全星质量管…