Java浮点数舍入异常:0.075按HALF_UP规则未达预期
度量转换器舍入逻辑问题排查
我需要实现一个小型度量转换器类,核心任务是进行单位转换(例如米转英里)。验收要求为:结果保留1位小数精度,但若数值小于0.1,则保留2位小数精度。
我编写了如下舍入方法:
public static String round(double input) { DecimalFormat df; if(input < 0.1) { df = new DecimalFormat("#.##"); } else { df = new DecimalFormat("#.#"); } df.setRoundingMode(RoundingMode.HALF_UP); Locale currentLocale = LocaleContextHolder.getLocale(); df.setDecimalFormatSymbols(DecimalFormatSymbols.getInstance(currentLocale)); return df.format(input); }
随后编写了单元测试验证代码:
package de.helper; import org.junit.jupiter.api.Test; import org.springframework.context.i18n.LocaleContextHolder; import java.util.Locale; import static org.assertj.core.api.Assertions.assertThat; class MetricsConverterTest { @Test public void roundNumberWith1DigitAccuracy() { //arrange LocaleContextHolder.setLocale(Locale.GERMAN); //act String actualRoundDown = MetricsConverter.round(10.123456); String actualRoundUp = MetricsConverter.round(10.153456); //assert assertThat(actualRoundDown).isEqualTo("10,1"); assertThat(actualRoundUp).isEqualTo("10,2"); } @Test public void roundSmallNumberWith2DigitAccuracy() { //arrange LocaleContextHolder.setLocale(Locale.GERMAN); //act String actualRoundDown = MetricsConverter.round(0.091); String actualRoundUp = MetricsConverter.round(0.076); //assert assertThat(actualRoundDown).isEqualTo("0,09"); assertThat(actualRoundUp).isEqualTo("0,08"); } @Test public void roundSmallNumberWith2DigitAccuracyStrangeEdgeCase() { //arrange LocaleContextHolder.setLocale(Locale.GERMAN); //act String actualRoundUp = MetricsConverter.round(0.075); //assert assertThat(actualRoundUp).isEqualTo("0,08"); } @Test public void roundSmallNumberEdgeCase1DigitAccuracy() { //arrange LocaleContextHolder.setLocale(Locale.GERMAN); //act String actualRoundUp = MetricsConverter.round(0.095); //assert assertThat(actualRoundUp).isEqualTo("0,1"); } }
但测试方法roundSmallNumberWith2DigitAccuracyStrangeEdgeCase执行失败:传入0.075时,实际输出为"0,07",而预期应为"0,08",请问忽略了什么问题?
问题根源:浮点数精度损失
double是二进制浮点数,无法精确表示所有十进制小数。你传入的0.075在二进制中是无限循环小数,实际存储的是略小于0.075的近似值(比如0.074999999999999997...)。当DecimalFormat用HALF_UP模式舍入时,这个近似值会被判定为小于0.075,因此向下舍入到0.07,而非预期的0.08。
解决方案:使用BigDecimal避免精度问题
改用BigDecimal处理精确的十进制运算,它能准确表示十进制小数,规避二进制浮点数的精度损失。修改后的舍入方法如下:
public static String round(double input) { BigDecimal value = BigDecimal.valueOf(input); DecimalFormat df; if (value.compareTo(BigDecimal.valueOf(0.1)) < 0) { df = new DecimalFormat("#.##"); } else { df = new DecimalFormat("#.#"); } df.setRoundingMode(RoundingMode.HALF_UP); Locale currentLocale = LocaleContextHolder.getLocale(); df.setDecimalFormatSymbols(DecimalFormatSymbols.getInstance(currentLocale)); return df.format(value); }
更严谨的实现方式,直接用BigDecimal的舍入逻辑处理后再格式化:
public static String round(double input) { BigDecimal value = BigDecimal.valueOf(input); BigDecimal roundedValue; if (value.compareTo(BigDecimal.valueOf(0.1)) < 0) { roundedValue = value.setScale(2, RoundingMode.HALF_UP); } else { roundedValue = value.setScale(1, RoundingMode.HALF_UP); } DecimalFormat df = new DecimalFormat(); Locale currentLocale = LocaleContextHolder.getLocale(); df.setDecimalFormatSymbols(DecimalFormatSymbols.getInstance(currentLocale)); df.applyPattern(roundedValue.scale() == 2 ? "#.##" : "#.#"); return df.format(roundedValue); }
验证修改
修改后重新运行测试,roundSmallNumberWith2DigitAccuracyStrangeEdgeCase会正常通过:BigDecimal.valueOf(0.075)能准确表示该十进制数,经HALF_UP舍入后得到0.08,在德语locale下格式化输出"0,08"。
内容的提问来源于stack exchange,提问作者MrX
相关产品推荐
相关产品推荐

