You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.29 02:28:15