杰瑞科技汇

Java读取Excel小数不精确?

几乎可以肯定不是Excel文件本身的问题,而是由浮点数在计算机中的二进制存储方式以及Java库的解析过程共同导致的。

Java读取Excel小数不精确?-图1
(图片来源网络,侵删)

下面我将详细解释原因,并提供几种可靠的解决方案。


问题根源:为什么会出现不精确?

核心原因:浮点数的二进制表示(IEEE 754标准)

计算机内部不直接存储我们熟悉的十进制数,而是使用二进制,很多十进制小数在转换为二进制时是无限循环小数

最经典的例子就是 1

  • 十进制的 1 转换为二进制是 000110011001100110011... (无限循环)。
  • 计算机的浮点数(如 floatdouble)有固定的存储位数,无法表示这个无限循环的二进制小数。
  • 计算机只能存储一个最接近它的近似值。

当Java从Excel中读取这个值时,它首先会尝试将Excel的字符串或数字表示转换成Java的 double 类型,这个转换过程本身就会引入这个微小的误差。

Java读取Excel小数不精确?-图2
(图片来源网络,侵删)

Java库(如Apache POI)的角色

像Apache POI这样的库,在读取Excel单元格时,会根据单元格的数据类型返回一个Java对象。

  • 如果Excel单元格是数字格式,POI通常会返回一个 Double 对象。
  • 这个 Double 对象的值,就是上面提到的那个计算机内部的二进制近似值。

当你用 System.out.println() 打印这个 Double 时,为了方便阅读,Java会尝试将其转换回十进制字符串,如果这个二进制近似值在转换回十进制时产生了很长的尾数,你就会看到类似 989999999999995 这样的结果。

总结一下流程: Excel中的精确值 (15.99) -> POI读取 -> 转换为Java的Double (近似值) -> 打印输出 -> 显示为不精确的十进制


解决方案

有几种行之有效的方法来解决这个问题,从最推荐到备选方案排列。

Java读取Excel小数不精确?-图3
(图片来源网络,侵删)

使用 BigDecimal (最推荐、最精确)

BigDecimal 是Java中用于精确表示任意大小十进制数的类,它可以完美避免浮点数精度问题。

步骤:

  1. 不要直接将POI返回的 Double 赋值给 double 基本类型。
  2. 使用 BigDecimalvalueOf()String 构造函数来创建 BigDecimal 对象。

代码示例:

import org.apache.poi.ss.usermodel.*;
import java.io.FileInputStream;
import java.math.BigDecimal;
public class ExcelReadBigDecimal {
    public static void main(String[] args) throws Exception {
        // 1. 加载Excel文件
        FileInputStream fis = new FileInputStream("your_excel_file.xlsx");
        Workbook workbook = WorkbookFactory.create(fis);
        Sheet sheet = workbook.getSheetAt(0); // 获取第一个工作表
        // 2. 假设我们要读取第2行第1列的单元格 (索引从0开始)
        Row row = sheet.getRow(1);
        Cell cell = row.getCell(0);
        // 3. 使用BigDecimal进行精确读取和计算
        // --- 推荐做法 ---
        // 将单元格的数字值作为字符串传递给BigDecimal,这是最精确的方式
        String cellValueStr = cell.getStringCellValue(); // 先读取为字符串
        if (cellValueStr != null && !cellValueStr.isEmpty()) {
            BigDecimal decimalValue = new BigDecimal(cellValueStr);
            System.out.println("使用BigDecimal(String构造函数): " + decimalValue);
        }
        // --- 备选做法 ---
        // 如果直接使用POI返回的Double,虽然能解决打印问题,但底层精度问题依然存在
        // 在某些计算场景下可能不精确
        if (cell.getCellType() == CellType.NUMERIC) {
            double doubleValue = cell.getNumericCellValue();
            BigDecimal decimalValueFromDouble = BigDecimal.valueOf(doubleValue);
            System.out.println("使用BigDecimal.valueOf(double): " + decimalValueFromDouble);
        }
        workbook.close();
        fis.close();
    }
}

为什么推荐 new BigDecimal(String) 它直接解析Excel中显示的字符串形式的数字,完全绕过了 double 类型的二进制近似问题,是精度最高的方法。


使用 DecimalFormat 格式化输出 (如果只是显示问题)

如果你的问题仅仅是“显示”不精确,而后续的业务逻辑计算本身可以容忍微小的误差,那么你只需要在最终显示或输出时进行格式化即可。

代码示例:

import org.apache.poi.ss.usermodel.*;
import java.io.FileInputStream;
import java.text.DecimalFormat;
public class ExcelReadFormat {
    public static void main(String[] args) throws Exception {
        FileInputStream fis = new FileInputStream("your_excel_file.xlsx");
        Workbook workbook = WorkbookFactory.create(fis);
        Sheet sheet = workbook.getSheetAt(0);
        Row row = sheet.getRow(1);
        Cell cell = row.getCell(0);
        if (cell.getCellType() == CellType.NUMERIC) {
            double value = cell.getNumericCellValue();
            // 创建一个格式化对象,保留两位小数
            DecimalFormat df = new DecimalFormat("#.##");
            // 格式化后输出,它会进行四舍五入
            String formattedValue = df.format(value);
            System.out.println("原始 double 值: " + value); // 可能打印 15.989999999999995
            System.out.println("格式化后显示: " + formattedValue); // 打印 15.99
        }
        workbook.close();
        fis.close();
    }
}

注意: 这种方法只解决了显示问题,如果你拿这个 value 去进行复杂的数学运算(如累加),误差会累积放大。


检查Excel单元格格式 (源头排查)

虽然99%的情况是浮点数问题,但偶尔也可能是Excel文件本身的问题,确保Excel单元格的格式被正确设置为“数字”或“货币”,而不是“文本”,如果单元格被设置为文本格式,POI可能会将其作为字符串读取,从而避免这个问题,但你需要手动处理字符串到数字的转换。


  1. 首选 BigDecimal:对于任何涉及金钱、财务、科学计算等对精度要求高的场景,始终使用 BigDecimal

    • 最佳实践:使用 new BigDecimal(cell.getStringCellValue()) 来获取最精确的值。
    • 次佳实践:使用 BigDecimal.valueOf(cell.getNumericCellValue()),这比直接使用 double 好,但精度略低于前者。
  2. 仅用于显示时用 DecimalFormat:如果只是前端展示或日志打印,对底层计算精度无要求,使用 DecimalFormat 是最简单快捷的。

  3. 避免使用 float:在Java中,处理浮点数时,double 的精度远高于 float,除非有特殊的内存限制,否则应优先使用 doubleBigDecimal,避免使用 float

  4. 注意POI版本:确保你使用的是较新版本的Apache POI,旧版本可能在某些边缘情况下有bug,但核心的浮点数问题在所有版本中都存在。

通过以上方法,你就可以彻底解决Java读取Excel小数不精确的问题了。

分享:
扫描分享到社交APP
上一篇
下一篇