当前位置: 首页 > news >正文

Java中 0.05 + 0.01 ≠ 0.06 揭秘浮点数精度陷阱

目录

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

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

问题现象

令人困惑的计算结果

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.dtcms.com/a/351249.html

相关文章:

  • VSCode: 从插件安装到配置,如何实现 Ctrl+S 保存时,完全按照 .eslintrc.js 中的 ESLint 规则自动格式化代码
  • vscode 配置 + androidStudio配置
  • Easy Voice Recorder Pro v2.9.3 简单易用的专业音频录制工具应用
  • 开发手札:UnrealEngine编辑器开发
  • 基于stm32的物联网OneNet火灾报警系统
  • Java面试指南‌——事务:数据库世界的超级英雄联盟
  • OpenSCA开源社区每日安全漏洞及投毒情报资讯|22th-24th Aug. , 2025
  • MySQL基本语法及与JAVA程序建立连接
  • 设计模式七大原则附C++正反例源码
  • 学习嵌入式的第三十八天
  • 【网络安全】XSS漏洞——PortSwigger靶场-DOM破坏
  • 常见的 Loader 和 Plugin?
  • 观察者模式 (Observer Pattern)与几个C++应用例子
  • Visual Basic 数据类型应用示例
  • EasyExcel 3.x 导出动态表头,动态sheet页
  • 国产化Excel开发组件Spire.XLS教程:Python 读取 CSV 文件,从基础到进阶指南
  • C shell 学习
  • AI出题人给出的Java后端面经(二十仨)(不定更)
  • 线性代数中矩阵等价与离散数学中关系的闭包之间的关联
  • dapo:开源大规模llm强化学习系统的突破与实现
  • AI提示词30天入门培训计划
  • STM32物联网项目---ESP8266微信小程序结合OneNET平台MQTT实现STM32单片机远程智能控制---MQTT篇(三)
  • 【密集目标检测】停车场车辆(车位)识别数据集:12k+图像,yolo标注
  • 从GPT-5发布来分析LLM大模型幻觉收敛(一)
  • 广告网站与Coze智能体集成
  • 节能率的图表组件的选择
  • MT** 时间指标全景图:从可靠性到可维护性的度量体系
  • PEFT 模型解析(59)
  • Linux 详谈库制作与原理
  • python中生成器