慎用Float和Double进行浮点运算

背景

在之前的开发过程中, 遇到了一些小问题.
就是在某功能计算时, 按照当时的设想是需要保留两位小数并向下取整.
当时没有太好的思路, 于是请教了好朋友gpt同志.

而gpt给出3种思路:

  • 使用String.format方法

    double value = 123.456789;  
    String formattedString = String.format("%.2f", value);  
    System.out.println(formattedString); // 输出 "123.46"
    
  • DecimalFormat

    import java.text.DecimalFormat;  
     
    double value = 123.456789;  
    DecimalFormat decimalFormat = new DecimalFormat("#.##");  
    String formattedString = decimalFormat.format(value);  
    System.out.println(formattedString); // 输出 "123.46"
    
  • 如果你只是想要进行数值计算,并且想要得到一个近似到两位小数的double值(而不是字符串表示),那么你可以使用Math.round方法配合乘以100、除以100的操作

    double value = 123.456789;  
    double roundedValue = (double) Math.floor(value * 100) / 100;  
    System.out.println(roundedValue); // 输出可能接近 "123.46" 但可能由于精度问题不完全相等
    

当时由于业务需要计算选择了思路3, 当时天真的以为精度肯定不会对自己的业务造成影响.
因为自己只计算简单的求和以及平均值计算, 应该不会有啥大问题
可没想还是绳子专挑细处断, 再一次体验到墨菲定律…

问题描述

将主要业务问题抽象成代码表示就是:
需要将Double类型集合求平均值, 然后根据实际的count数计算出总和评分, 需要向下取整并保留两位小数

    public static void main(String[] args) {
        //23D->23 BigDecimal 出现了精度丢失
        List<Double> list = Arrays.asList(
                1D,
                4D,
                3D,
                3D,
                3D,
                3D,
                1D,
                1D,
                2D,
                2D);
        System.out.println(calculateAverage(list));  //2.30
        //这里指的是数据库中没有数据, 即只有当前数据
        long count = 1;
        Double rating = (calculateAverage(list).doubleValue() / count) * 100;
        System.out.println(rating);  //229.99999999999997
        System.out.println(Math.floor(rating)/100);  //2.29
    }

    /**
     * 求出list中数组的平均值
     * @param numberList
     * @return
     */
    public static BigDecimal calculateAverage(List<Double> numberList) {
        if (numberList == null || numberList.isEmpty()) {
            throw new IllegalArgumentException("传入数组不能为空");
        }
        int sum = 0;
        for (Double number : numberList) {
            sum += number;
        }
        BigDecimal bd = new BigDecimal(String.valueOf((double) sum / numberList.size()));
        bd = bd.setScale(2, RoundingMode.FLOOR); // 保留两位小数,向下取整
        return bd;
    }

可以看到Double类型的数值 2.3D在乘以100之后结果不是预想中的230, 而是 229.99999999999997
而且, 在将2.3D依次乘以1, 10, 100, 1000 时, 唯有在乘以100的时候出现了精度丢失
到底是为什么呢?
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

经过查阅资料 java数值范围以及float与double精度丢失问题 后, 从下面这部分我看到了

//举例:
double result = 1.0 - 0.9;   //0.09999999999999998

这是java和其它计算机语言都会出现的问题,下面我们分析一下为什么会出现这个问题:
float和double类型主要是为了科学计算和工程计算而设计的。他们执行二进制浮点运算,这是为了在广泛的数字范围上提供较为精确的快速近似计算而精心设计的。然而,它们并没有提供完全精确的结果,所以我们不应该用于精确计算的场合。float和double类型尤其不适合用于货币运算,因为要让一个float或double精确的表示0.1或者10的任何其他负数次方值是不可能的(其实道理很简单,十进制系统中能不能准确表示出1/3呢?同样二进制系统也无法准确表示1/10)。
浮点运算很少是精确的,只要是超过精度能表示的范围就会产生误差。往往产生误差不是因为数的大小,而是因为数的精度。因此,产生的结果接近但不等于想要的结果。尤其在使用 float 和 double 作精确运算的时候要特别小心。

我的理解就是float和double在计算时, 都会转换成二进制计算
由于小数点后面的数在转换成二进制后, 很多都无法转换成有限位二进制数,
例如: 0.3->0.0100110011001100110011001100110011001100110011001101
(如下图,结果是无限循环,只是执行到指定位数不进行显示了)

在这里插入图片描述

然后我们反过来将二进制转化成十进制进行计算, 可以看到得出的只是近似且无限接近 0.3 的值

在这里插入图片描述
因此在进行计算时精度会有不同程度的丢失. 所以就会在计算时得到我们所不期望的很长为数的小数(0.30000000000000004)
即在进行带有小数的double或者float数值进行计算(加减乘除)时, 因为精度的原因会导致数据结果有误
故float或者double在进行浮点数运算时, 一般只能无限但接近我们数学运算得到的结果!

解决方案

  1. 将小数点部分也完全按照整数进行存储和计算

    //举例:
    double result = 1.0 - 0.9;   //0.09999999999999998
    //需要保留一位小数时, 结果统一乘10再除以10
    double result = (10-9)/10
    
  2. 使用BigDecmal进行加减乘除
    需要注意的是,创建BigDecimal最好是使用字符串。否则也有可能出现精度丢失问题,例如上面说的0.58*100,如果不是字符串,依然精度丢失.

    //修改后进行计算
     BigDecimal countB = new BigDecimal("1");
     BigDecimal ratingB = new BigDecimal("2.3");
     BigDecimal dotNum = new BigDecimal("100");
     System.out.println(ratingB.divide(countB).multiply(dotNum).setScale(2, RoundingMode.FLOOR)
             .divide(dotNum).doubleValue());  //2.3
    
  3. 利用工具类, 封装计算方法, 将两个传入的double值进行计算

    double div = BigDecimalUtils.div(2.3, 1, 2, BigDecimal.ROUND_FLOOR);
    double mul = BigDecimalUtils.mul(div, 100);
    double mul2 = BigDecimalUtils.div(mul, 100);
    System.out.println("mul = " + mul);  //mul = 230.0
    System.out.println("mul2 = " + mul2);  //mul2 = 2.3
    

附:工具类代码

下面是对double进行浮点运算的工具类代码
没有特别对float的运算封装成工具类, 因此使用时可以将float转成double来进行计算

import java.math.BigDecimal;

/**
 * info:提供对double类型参数进行各种浮点运算的工具类
 *
 * @Author chy
 * @Date 2024/07/09 16:44
 */
public class BigDecimalUtils {
    /**
     * 默认除法运算精度为小数点后两位
     */
    private static final int DEF_DIV_SCALE = 2;
    private BigDecimalUtils() {
    }

    /**
     * 提供精确的加法运算
     *
     * @param v1 被加数
     * @param v2 加数
     * @return 两个参数的和
     */
    public static double add(double v1, double v2) {
        BigDecimal b1 = new BigDecimal(Double.toString(v1));
        BigDecimal b2 = new BigDecimal(Double.toString(v2));
        return b1.add(b2).doubleValue();
    }

    /**
     * 提供精确的减法运算
     *
     * @param v1 被减数
     * @param v2 减数
     * @return 两个参数的差
     */
    public static double sub(double v1, double v2) {
        BigDecimal b1 = new BigDecimal(Double.toString(v1));
        BigDecimal b2 = new BigDecimal(Double.toString(v2));
        return b1.subtract(b2).doubleValue();
    }

    /**
     * 提供精确的乘法运算
     *
     * @param v1 被乘数
     * @param v2 乘数
     * @return 两个参数的积
     */
    public static double mul(double v1, double v2) {
        BigDecimal b1 = new BigDecimal(Double.toString(v1));
        BigDecimal b2 = new BigDecimal(Double.toString(v2));
        return b1.multiply(b2).doubleValue();
    }

    /**
     * 提供对结果进行各种舍入处理的乘法运算
     *
     * @param v1           被乘数
     * @param v2           乘数
     * @param scale        表示表示需要精确到小数点以后几位。
     * @param roundingMode 舍入模式: {@link java.math.BigDecimal#ROUND_UP}
     * @return 两个参数的积
     */
    public static double mul(double v1, double v2, int scale, int roundingMode) {
        BigDecimal b1 = new BigDecimal(Double.toString(v1));
        BigDecimal b2 = new BigDecimal(Double.toString(v2));
        return b1.multiply(b2).setScale(scale, roundingMode).doubleValue();
    }

    /**
     * 提供(相对)精确的除法运算,精确到小数点以后2位,默认进行四舍五入
     *
     * @param v1 被除数
     * @param v2 除数
     * @return 两个参数的商
     */
    public static double div(double v1, double v2) {
        return div(v1, v2, DEF_DIV_SCALE, BigDecimal.ROUND_HALF_UP);
    }

    /**
     * 提供各种舍入模式的的除法运算。当发生除不尽的情况时,由scale参数指定精度
     *
     * @param v1           被除数
     * @param v2           除数
     * @param scale        表示表示需要精确到小数点以后几位。
     * @param roundingMode {@link java.math.BigDecimal#ROUND_UP}
     * @return 两个参数的商
     */
    public static double div(double v1, double v2, int scale, int roundingMode) {
        if (scale < 0) {
            throw new IllegalArgumentException(
                    "The scale must be a positive integer or zero");
        }
        BigDecimal b1 = new BigDecimal(Double.toString(v1));
        BigDecimal b2 = new BigDecimal(Double.toString(v2));
        return b1.divide(b2, scale, roundingMode).doubleValue();
    }

    /**
     * 提供精确的小数位四舍五入处理。
     *
     * @param v     需要四舍五入的数字
     * @param scale 小数点后保留几位
     * @return 四舍五入后的结果
     */
    public static double round(double v, int scale) {

        if (scale < 0) {
            throw new IllegalArgumentException(
                    "The scale must be a positive integer or zero");
        }
        BigDecimal b = new BigDecimal(Double.toString(v));
        BigDecimal one = new BigDecimal("1");
        return b.divide(one, scale, BigDecimal.ROUND_HALF_UP).doubleValue();
    }
};

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

时间静止不是简史

感谢你的肯定, 我将继续努力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值