浮点数精度问题

核心原因:

二进制无法精确表示某些十进制小数
计算机使用 IEEE 754 标准的二进制浮点数存储,但不是所有十进制小数都能被精确转换为二进制。

会出现精度问题的数字:

能精确表示的小数特征:

一个小数能被 IEEE 754 double 精确表示,当且仅当它可以表示为:

复制代码
m / 2^n (其中 m 和 n 都是整数)

示例:

复制代码
0.5  = 1/2    = 1/2^1   ✅ 可以精确表示
0.25 = 1/4    = 1/2^2   ✅ 可以精确表示
0.75 = 3/4    = 3/2^2   ✅ 可以精确表示
5.5  = 11/2   = 11/2^1  ✅ 可以精确表示

0.1  = 1/10             ❌ 分母含有因子5,无法精确表示
0.2  = 1/5              ❌ 分母含有因子5,无法精确表示
1.1  = 11/10            ❌ 分母含有因子5,无法精确表示
2.2  = 22/10 = 11/5     ❌ 分母含有因子5,无法精确表示
3.3  = 33/10            ❌ 分母含有因子5,无法精确表示

为什么 POI 4.1.2 有时"隐藏"了这个问题?

POIcell.setCellType(CellType.STRING) 方法会对数值进行隐式的格式化 ,它会:

检查数值是否"接近 "某个简单的小数

如果是,就输出那个简单的小数

但如果误差超过某个阈值 ,就会暴露完整精度
这就是为什么:

有些 1.1 显示为 "1.1"(POI 做了四舍五入)

有些 1.1 显示为 "1.1000000000000001"(POI 没有做四舍五入)
这种行为是不确定的,依赖于

POI 版本

Excel 文件 的创建方式

单元格的历史操作(是否经过公式计算、复制粘贴等)

测试:

java 复制代码
public class FloatingPointTest {
    public static void main(String[] args) {
        double[] values = {1, 1.1, 2, 2.2, 3, 3.3, 4, 5, 5.5, 6, 6.6, 7, 8, 8.8, 9, 9.9};
        
        System.out.println("=== 浮点数精度测试 ===\n");
        
        for (double value : values) {
            String strValue = String.valueOf(value);
            BigDecimal bd = BigDecimal.valueOf(value);
            String preciseStr = bd.toPlainString();
            
            boolean hasPrecisionIssue = !strValue.equals(preciseStr);
            
            System.out.printf("值: %4.1f | String.valueOf: %20s | BigDecimal: %20s | %s%n",
                value, strValue, preciseStr, 
                hasPrecisionIssue ? "❌ 有精度问题" : "✅ 正常");
        }
        
        // 测试累加计算
        System.out.println("\n=== 累加计算精度测试 ===\n");
        testAddition("1.1", "2.2");
        testAddition("1.1", "3.3");
        testAddition("1.1", "4.4");
        testAddition("1.1", "5.5");
        testAddition("1.1", "6.6");
        testAddition("1.1", "7.7");

        testAddition("5.5", "3.3");
        testAddition("8.8", "1.1");
    }
    
    private static void testAddition(String a, String b) {
        // 错误的方式:使用 Double
        double sumDouble = Double.parseDouble(a) + Double.parseDouble(b);
        
        // 正确的方式:使用 BigDecimal
        BigDecimal sumBD = new BigDecimal(a).add(new BigDecimal(b));
        
        System.out.printf("%s + %s = %s (Double) vs %s (BigDecimal)%n",
            a, b, sumDouble, sumBD.setScale(2, RoundingMode.HALF_UP));
    }
}

输出:

java 复制代码
=== 浮点数精度测试 ===

值:  1.0 | String.valueOf:                  1.0 | BigDecimal:                  1.0 | ✅ 正常
值:  1.1 | String.valueOf:                  1.1 | BigDecimal:                  1.1 | ✅ 正常
值:  2.0 | String.valueOf:                  2.0 | BigDecimal:                  2.0 | ✅ 正常
值:  2.2 | String.valueOf:                  2.2 | BigDecimal:                  2.2 | ✅ 正常
值:  3.0 | String.valueOf:                  3.0 | BigDecimal:                  3.0 | ✅ 正常
值:  3.3 | String.valueOf:                  3.3 | BigDecimal:                  3.3 | ✅ 正常
值:  4.0 | String.valueOf:                  4.0 | BigDecimal:                  4.0 | ✅ 正常
值:  5.0 | String.valueOf:                  5.0 | BigDecimal:                  5.0 | ✅ 正常
值:  5.5 | String.valueOf:                  5.5 | BigDecimal:                  5.5 | ✅ 正常
值:  6.0 | String.valueOf:                  6.0 | BigDecimal:                  6.0 | ✅ 正常
值:  6.6 | String.valueOf:                  6.6 | BigDecimal:                  6.6 | ✅ 正常
值:  7.0 | String.valueOf:                  7.0 | BigDecimal:                  7.0 | ✅ 正常
值:  8.0 | String.valueOf:                  8.0 | BigDecimal:                  8.0 | ✅ 正常
值:  8.8 | String.valueOf:                  8.8 | BigDecimal:                  8.8 | ✅ 正常
值:  9.0 | String.valueOf:                  9.0 | BigDecimal:                  9.0 | ✅ 正常
值:  9.9 | String.valueOf:                  9.9 | BigDecimal:                  9.9 | ✅ 正常

=== 累加计算精度测试 ===

1.1 + 2.2 = 3.3000000000000003 (Double) vs 3.30 (BigDecimal)
1.1 + 3.3 = 4.4 (Double) vs 4.40 (BigDecimal)
1.1 + 4.4 = 5.5 (Double) vs 5.50 (BigDecimal)
1.1 + 5.5 = 6.6 (Double) vs 6.60 (BigDecimal)
1.1 + 6.6 = 7.699999999999999 (Double) vs 7.70 (BigDecimal)
1.1 + 7.7 = 8.8 (Double) vs 8.80 (BigDecimal)
5.5 + 3.3 = 8.8 (Double) vs 8.80 (BigDecimal)
8.8 + 1.1 = 9.9 (Double) vs 9.90 (BigDecimal)

Excel导入精度损失可能的原因:

1. Excel 文件的创建方式不同

如果客户环境的 Excel 文件是:

从其他系统导出 (如 SAP、Oracle)

通过程序生成 (如 Apache POI、EPPlus)

经过多次复制粘贴

这些操作会影响单元格内部的存储格式标记,导致 POI 采用不同的转换策略。
2. Excel 版本差异

不同版本的 Excel (2010、2013、2016、2019、365)在保存 .xlsx 文件时,对数值的序列化方式略有不同。
3. 单元格格式设置

即使看起来都是"常规"格式,内部可能有细微差别:

曾经设置为"数值"格式 ,后改回"常规"

应用过条件格式

使用过数据验证

解决方法:

java 复制代码
    /**
     * 将单元格内容转为字符串,如果null则返回空串
     *
     * @param cell Excel单元格
     * @return 字符串值
     */
    public static String getStringValue(Cell cell) {
        if (cell == null) {
            return "";
        }
        switch (cell.getCellType()) {
            case STRING:
                return cell.getStringCellValue().trim();
            case NUMERIC:
                // ✅ 日期类型特殊处理
                if (DateUtil.isCellDateFormatted(cell)) {
                    return cell.getDateCellValue().toString();
                } else {
                    // ✅ 数值类型:使用 BigDecimal 精确转换
                    double numericValue = cell.getNumericCellValue();
                    
                    // 如果是整数,不显示小数点
                    if (numericValue == Math.floor(numericValue) && !Double.isInfinite(numericValue)) {
                        return String.valueOf((long) numericValue);
                    } else {
                        // 使用 BigDecimal 避免精度问题
                        // stripTrailingZeros() 去除末尾的0
                        // toPlainString() 避免科学计数法
                        return BigDecimal.valueOf(numericValue)
                                .stripTrailingZeros()
                                .toPlainString();
                    }
                }
            case BOOLEAN:
                return String.valueOf(cell.getBooleanCellValue());
            case FORMULA:
                // ✅ 公式类型:尝试获取字符串值,失败则按数值处理
                try {
                    return cell.getStringCellValue();
                } catch (Exception e) {
                    double formulaValue = cell.getNumericCellValue();
                    if (formulaValue == Math.floor(formulaValue) && !Double.isInfinite(formulaValue)) {
                        return String.valueOf((long) formulaValue);
                    } else {
                        return BigDecimal.valueOf(formulaValue)
                                .stripTrailingZeros()
                                .toPlainString();
                    }
                }
            default:
                return "";
        }
    }
相关推荐
小白|1 小时前
cmake:昇腾CANN构建系统完全指南
java·c++·算法
weixin_512976171 小时前
Java 面试宝典 Beta5.0
java
Ting-yu1 小时前
Spring AI Alibaba零基础速成(5) ---- Memory(记忆)
java·人工智能·后端·spring
月落归舟1 小时前
一文掌握Spring AOP:从入门到底层原理
java·后端·spring
QuZhengRong1 小时前
【Luck-Report】缓存
java·前端·后端·vue·excel
XiYang-DING1 小时前
【Spring】SpringMVC
java·后端·spring
想学习java初学者1 小时前
SpringBoot整合GS1编码解码
java·spring boot·后端
日月云棠1 小时前
2 快速入门实战指南
java·后端
日月云棠1 小时前
3 Dubbo 2.7 高级配置:检查控制、版本策略与协议选择
java·后端
砍材农夫2 小时前
物联网 基于netty构建mqtt协议规范(主题通配符订阅)
java·前端·javascript·物联网·netty