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

下面我将详细解释原因,并提供几种可靠的解决方案。
问题根源:为什么会出现不精确?
核心原因:浮点数的二进制表示(IEEE 754标准)
计算机内部不直接存储我们熟悉的十进制数,而是使用二进制,很多十进制小数在转换为二进制时是无限循环小数。
最经典的例子就是 1:
- 十进制的
1转换为二进制是000110011001100110011...(无限循环)。 - 计算机的浮点数(如
float和double)有固定的存储位数,无法表示这个无限循环的二进制小数。 - 计算机只能存储一个最接近它的近似值。
当Java从Excel中读取这个值时,它首先会尝试将Excel的字符串或数字表示转换成Java的 double 类型,这个转换过程本身就会引入这个微小的误差。

Java库(如Apache POI)的角色
像Apache POI这样的库,在读取Excel单元格时,会根据单元格的数据类型返回一个Java对象。
- 如果Excel单元格是数字格式,POI通常会返回一个
Double对象。 - 这个
Double对象的值,就是上面提到的那个计算机内部的二进制近似值。
当你用 System.out.println() 打印这个 Double 时,为了方便阅读,Java会尝试将其转换回十进制字符串,如果这个二进制近似值在转换回十进制时产生了很长的尾数,你就会看到类似 989999999999995 这样的结果。
总结一下流程:
Excel中的精确值 (15.99) -> POI读取 -> 转换为Java的Double (近似值) -> 打印输出 -> 显示为不精确的十进制
解决方案
有几种行之有效的方法来解决这个问题,从最推荐到备选方案排列。

使用 BigDecimal (最推荐、最精确)
BigDecimal 是Java中用于精确表示任意大小十进制数的类,它可以完美避免浮点数精度问题。
步骤:
- 不要直接将POI返回的
Double赋值给double基本类型。 - 使用
BigDecimal的valueOf()或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可能会将其作为字符串读取,从而避免这个问题,但你需要手动处理字符串到数字的转换。
-
首选
BigDecimal:对于任何涉及金钱、财务、科学计算等对精度要求高的场景,始终使用BigDecimal。- 最佳实践:使用
new BigDecimal(cell.getStringCellValue())来获取最精确的值。 - 次佳实践:使用
BigDecimal.valueOf(cell.getNumericCellValue()),这比直接使用double好,但精度略低于前者。
- 最佳实践:使用
-
仅用于显示时用
DecimalFormat:如果只是前端展示或日志打印,对底层计算精度无要求,使用DecimalFormat是最简单快捷的。 -
避免使用
float:在Java中,处理浮点数时,double的精度远高于float,除非有特殊的内存限制,否则应优先使用double或BigDecimal,避免使用float。 -
注意POI版本:确保你使用的是较新版本的Apache POI,旧版本可能在某些边缘情况下有bug,但核心的浮点数问题在所有版本中都存在。
通过以上方法,你就可以彻底解决Java读取Excel小数不精确的问题了。
