慎用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.99999999999997System.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();}
};

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/874868.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Redis从入门到超神-(五)Redis实现分布式锁原理

引言 什么是分布式锁&#xff1f; 分布式锁是分布式系统中用于控制多个进程或线程对共享资源的访问的一种机制。在分布式系统中&#xff0c;由于存在多个服务实例或节点&#xff0c;它们可能会同时尝试访问或修改同一份数据或资源。如果没有适当的同步机制&#xff0c;就可能导…

Android 系统下的log文件【转载】

Android系统中的log都有那些&#xff0c;如何在JNI或者自己在Android系统下开发的程序中打印Log 前言 Android 系统中的Log分为两类&#xff0c;一种是java层的&#xff0c; 一种是Native层的&#xff0c; Java层的Log Log.dLog.eLog.vLog.i 在此不做过多赘述, 最终还是通过…

labview实现两台电脑共享变量传输及同步

因为工作需要&#xff0c;需要实现多台主机间进行数据传输&#xff0c; 有两个备选方案&#xff0c; 1&#xff1a;建立tcp&#xff0c;然后自己解包 2&#xff1a;就是通过共享变量传输 虽然共享变量也是建立在TCP/IP上面的&#xff0c;但是不用自己解包呀 关于共享变量网络上…

KBQA调研——学术界

目录 1. 任务 1.1. 背景1.2. 任务定义1.3. 数据集1.4. SOTA1.5. 评测标准 2. 方法总结 2.1. 基于语义解析&#xff08;Semantic Parsing&#xff09;的方法2.2. 基于信息抽取&#xff08;Information Extraction&#xff09;的方法 2.2.1. 候选答案的得出2.2.2. 问题的信息抽取…

mysql面试(四)

前言 本章节有些长&#xff0c;主要的篇幅是介绍缓存页的算法&#xff0c;如何快速的定位哪些是没有用过的&#xff0c;哪些是用过的&#xff0c;哪些是要淘汰掉的。 建议可以阅读一下这里面LRU算法相关的内容&#xff0c;和很多组件里面基本原理都是想通的&#xff0c;比如re…

聊聊 C# 中的顶级语句

前言 在 C# 9.0 版本之前&#xff0c;即使只编写一行输出 “Hello world” 的 C# 代码&#xff0c;也需要创建一个 C# 类&#xff0c;并且需要为这个 C# 类添加 Main 方法&#xff0c;才能在 Main 方法中编写代码。从 C# 9.0 开始&#xff0c;C# 增加了 “顶级语句” 语法&…

【FFmpeg】avcodec_receive_frame函数

目录 1.avcodec_receive_frame1.1 返回解码帧&#xff08;ff_decode_receive_frame&#xff09;1.2 返回重建帧&#xff08;ff_encode_receive_frame&#xff09; 2.小结 FFmpeg相关记录&#xff1a; 示例工程&#xff1a; 【FFmpeg】调用ffmpeg库实现264软编 【FFmpeg】调用f…

Vue3.0有什么更新——前端面试

1、监测机制改变 带来基于代理proxy的observer实现&#xff0c;提供全语言覆盖的反应性跟踪 消除 Vue2中 基于Object.defineProperty的实现 所存在的许多限制 2、只能监测属性&#xff0c;不能监测对象 检测属性的 添加和删除 检测 数组索引和长度的变更 支持 Map、Set …

nginx 如何做针对 ip 的限流

限流策略的背景&#xff1a; 限流可以在单体应用或分布式应用中实现。可以使用API网关如Zuul、Kong等实现限流&#xff0c;避免在业务应用中实现限流逻辑。 使用Nginx进行限流的优势&#xff1a; Nginx或openResty可以配置限流&#xff0c;无需修改应用代码。通过Nginx配置实现…

阿里云图片文件上传

一,官网地址 https://help.aliyun.com/document_detail/84781.html一切依据于官网 二,导入依赖 <dependencies><!-- 阿里云oss依赖 --><dependency><groupId>com.aliyun.oss</groupId><artifactId>aliyun-sdk-oss</artifactId>&l…

redis原理之底层数据结构-跳表

1.什么是跳表 1.1 链表及其不足 链表是在程序设计中最常见的数据结构之一&#xff0c;它通过指针将多个链表节点连接起来&#xff0c;这样就可以将逻辑上同一类的数据存储到不连续的内存空间上。链表结构如下&#xff1a; 但是链表有一个问题&#xff0c;就是当链表需要查询一…

Windows系统网络流量监控与限制攻略

在现代互联网生活中&#xff0c;网络宽带流量管理变得越来越重要。特别是在家庭或小型办公环境中&#xff0c;有限的宽带流量需要被合理分配和有效利用。Windows系统提供了一些内置的工具和方法&#xff0c;可以帮助我们监控和限制电脑的网络宽带流量。 一、监控网络宽带流量 …

3.1 FreeRTOS详细移植步骤(自己的实操)

[TOC](3.1 FreeRTOS详细移植步骤(自己的实操)) 自己使用阿波罗F767的内存管理实验和定时器实验&#xff0c;进行复刻。 FreeRTOS源码版本是FreeRTOS 202212.01。官网和Github都有下载。 按照STM32F767FreeRTOS开发手册V1.1进行移植复刻。 注:这个开发手册不是开发指南。跟视频里…

关于Qt部署CMake导致“Failed to set working directory to”的问题

2024年7月23日补充&#xff1a;该目录过深的情况只在Win10上有发现&#xff0c;Win11则没有问题&#xff0c;且Win11可以在DevHome中设置LongPath。 --------------------------------------------------------------------------------------------------------------- 使用qt…

ADetailer模型+Stable Diffusion的inpainting功能是如何对遮罩区域进行修复生成的ADetailer

模型选则&#xff1a; face_yolov8n.pt 和 face_yolov8s.pt&#xff1a; 用途&#xff1a;用于人脸检测。特点&#xff1a;YOLOv8n 是轻量级版本&#xff0c;适合资源有限的设备&#xff1b;YOLOv8s 是标准版本&#xff0c;检测精度更高。 hand_yolov8n.pt&#xff1a; 用途&am…

Spark_Oracle_II_Spark高效处理Oracle时间数据:通过JDBC桥接大数据与数据库的分析之旅

接前文背景&#xff0c; 当需要从关系型数据库&#xff08;如Oracle&#xff09;中读取数据时&#xff0c;Spark提供了JDBC连接功能&#xff0c;允许我们轻松地将数据从Oracle等数据库导入到Spark DataFrame中。然而&#xff0c;在处理时间字段时&#xff0c;可能会遇到一些挑战…

分布式Apollo配置中心搭建实战

文章目录 环境要求第一步、软件下载第二步、创建数据库参考文档 最近新项目启动&#xff0c;采用Apollo作为分布式的配置中心&#xff0c;在本地搭建huanj 实现原理图如下所示。 环境要求 Java版本要求&#xff1a;JDK1.8 MySql版本要求&#xff1a;5.6.5 Apollo版本要求&…

第八讲:Sysmac Studio控制器设置

控制器设置 一、控制器设定-操作设置 1、启动模式(运行模式/编程模式) 控制器上电后,希望程序运行还是不运行。如果说希望程序运行,那么就选择运行模式。如果说希望上电后程序不运行就选择编程模式。 通常情况下选运行模式可能会比较多一些。 2、SD内存卡设置 当控制…

银河麒麟(arm64)环境下通过docker安装postgis3,并实现数据整体迁移

银河麒麟(arm64)环境下通过docker安装postgis3,并实现数据整体迁移 硬件配置:麒麟9006C 系统环境:银河麒麟桌面版v10 sp1 数据库:postgresql11+postgis3.0 具体的步骤参考https://blog.csdn.net/qq_34817440/article/details/103914574 -----主要操作-----------------…

QSqlTableModel操作数据库单表使用总结

本文记录使用QSqlTableModel等组件实现单表的数据库操作。 QSqlTableModel是一个模型类&#xff0c;它的实例可以作为一个数据表的模型。使用QSqlTableModel模型和QTableView组件构成模型/视图结构&#xff0c;就可以实现数据表的数据显示和编辑。 目录 所需的类及定义 表格控…