一、核心概念

1. 什么是浮点数?

Java 中的 float32位单精度浮点数,遵循 IEEE 754 标准,格式如下:

  • 1位符号位
  • 8位指数位
  • 23位尾数位(有效数字)

double 是 64位双精度浮点数,精度更高(约15-17位有效数字),float 约为 6-7位有效数字

2. 为什么会有精度问题?

  • 二进制无法精确表示所有十进制小数
    例如:0.1 在二进制中是无限循环小数(类似 1/3 = 0.333...),只能近似存储。
  • 有限位数限制精度
    float 只有23位尾数,超出部分会被舍入(rounding)
  • 运算过程累积误差
    多次加减乘除会放大初始误差。

3. 常见表现

System.out.println(0.1f + 0.2f); // 输出:0.30000001,而不是 0.3
System.out.println(0.1f == 0.1); // false!因为 0.1f (float) ≠ 0.1 (double)

二、操作步骤(详细实战指南)

✅ 步骤 1:识别浮点数精度问题

目标:判断当前计算是否可能受精度影响。

操作

  1. 检查是否涉及 非整数小数(如 0.1, 0.3, 0.7)
  2. 检查是否进行 多次加减或比较
  3. 检查是否使用 == 比较 float

示例代码

float a = 0.1f;
float b = 0.2f;
float sum = a + b;
System.out.println("Sum: " + sum); // 0.30000001 → 存在精度误差

✅ 步骤 2:使用容差(Epsilon)进行比较

目标:避免直接使用 == 比较浮点数。

操作

  1. 定义一个极小的容差值(epsilon),如 1e-6f
  2. 判断两数之差的绝对值是否小于 epsilon

公式|a - b| < ε

示例代码

public static boolean floatEquals(float a, float b, float epsilon) {
    return Math.abs(a - b) < epsilon;
}

// 使用
float a = 0.1f + 0.2f;
float expected = 0.3f;
boolean isEqual = floatEquals(a, expected, 1e-6f);
System.out.println("Are equal? " + isEqual); // true

✅ 步骤 3:使用 BigDecimal 处理高精度需求

目标:在金融、科学计算等场景避免精度丢失。

操作

  1. 使用 BigDecimal 构造函数传入 字符串(避免 float 本身已失真)
  2. 使用 add(), subtract(), multiply(), divide() 等方法
  3. 使用 setScale() 设置精度和舍入模式
  4. 使用 compareTo() 比较(而非 equals()

示例代码

import java.math.BigDecimal;
import java.math.RoundingMode;

public class HighPrecisionExample {
    public static void main(String[] args) {
        // ✅ 正确:用字符串构造
        BigDecimal a = new BigDecimal("0.1");
        BigDecimal b = new BigDecimal("0.2");
        BigDecimal sum = a.add(b);
        System.out.println("Sum: " + sum); // 0.3

        // 设置精度和舍入
        BigDecimal result = new BigDecimal("10").divide(new BigDecimal("3"), 4, RoundingMode.HALF_UP);
        System.out.println("10/3 ≈ " + result); // 3.3333
    }
}

✅ 步骤 4:使用 Math.ulp() 分析精度单位

目标:了解某个浮点数的“最后一位单位”,用于动态设置 epsilon。

操作

  1. 调用 Math.ulp(float x) 获取 x 的精度单位
  2. 用于设置合理的比较容差

示例代码

float x = 1.0f;
float ulp = Math.ulp(x);
System.out.println("ULP of 1.0: " + ulp); // 约 1.192e-7

// 动态 epsilon
float epsilon = ulp * 10; // 容忍 10 个 ULP 的误差

✅ 步骤 5:格式化输出控制显示精度

目标:避免控制台输出误导(如显示 0.30000001)

操作

  1. 使用 String.format()DecimalFormat
  2. 控制小数位数

示例代码

float value = 0.1f + 0.2f;
System.out.println(String.format("%.2f", value)); // 输出:0.30

// 或使用 DecimalFormat
import java.text.DecimalFormat;
DecimalFormat df = new DecimalFormat("#.##");
System.out.println(df.format(value)); // 0.3

三、常见错误

错误 示例 后果
直接用 == 比较 float 0.1f + 0.2f == 0.3f 返回 false,逻辑错误
float 构造 BigDecimal new BigDecimal(0.1f) 0.1f 本身已失真,仍不精确
忽视舍入模式 divide() 不指定模式 可能抛出 ArithmeticException
在循环中累积 float for (float x = 0; x < 1; x += 0.1f) 循环次数不准确,可能跳过或死循环
混用 floatdouble float a = 0.1; 编译错误(0.1 是 double)

四、注意事项

  1. float 精度有限:仅约 6-7 位有效数字,超过部分会丢失。
  2. 0.1f0.1:前者是 float,后者是 double,精度不同。
  3. Float.MIN_VALUE 是最小正数,不是最小值:最小值是 -Float.MAX_VALUE
  4. 浮点运算不满足结合律(a + b) + c 可能 ≠ a + (b + c)
  5. NaNInfinity:除零、开方负数等会产生特殊值,需用 isNaN(), isInfinite() 检查。

五、使用技巧

技巧 说明
✅ 优先使用 double 除非内存敏感,否则 double 精度更高,误差更小
✅ 用字符串初始化 BigDecimal 避免构造时就失真
✅ 使用 RoundingMode.HALF_UP 最常见的舍入方式(四舍五入)
✅ 用 compareTo() 比较 BigDecimal equals() 会比较精度(scale),compareTo() 比较数值
✅ 避免在循环中用 float 作索引 改用 int 控制,再转换

六、最佳实践与性能优化

实践 说明
金融计算必用 BigDecimal 避免一分钱误差
科学计算优先 double 精度更高,误差更小
普通计算可用 float,但避免比较 若必须比较,使用 epsilon
避免频繁创建 BigDecimal 可缓存常用值(如 BigDecimal.ZERO, BigDecimal.ONE
⚠️ BigDecimal 性能较低 float/double 慢 10-100 倍,仅在必要时使用
使用 Math.fma() 提高精度(Java 9+) 融合乘加操作,减少中间舍入误差

七、性能对比示例

// 性能:float > double >> BigDecimal
float f1 = 0.1f, f2 = 0.2f;
float sum1 = f1 + f2; // 快,但有误差

double d1 = 0.1, d2 = 0.2;
double sum2 = d1 + d2; // 稍慢,误差更小

BigDecimal bd1 = new BigDecimal("0.1");
BigDecimal bd2 = new BigDecimal("0.2");
BigDecimal sum3 = bd1.add(bd2); // 慢,但精确

八、总结

项目 关键点
精度问题根源 二进制无法精确表示十进制小数,有限位数导致舍入
典型表现 0.1 + 0.2 ≠ 0.3== 比较失败
解决方案 使用 epsilon 比较、BigDecimal、格式化输出
最佳实践 金融用 BigDecimal,科学用 double,普通用 float(慎比较)
性能排序 float > double > BigDecimal
核心口诀 不用 == 比浮点,BigDecimal 保精度,字符串构造是关键,epsilon 控比较差