慎用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;就可能导…

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

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

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# 增加了 “顶级语句” 语法&…

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

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

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内存卡设置 当控制…

Pytorch TensorBoard的使用

from torch.utils.tensorboard import SummaryWriter writer SummaryWriter("logs")for i in range(100):writer.add_scalar("yx",i,i) writer.close() 第一个参数 y2x: 这是图表的标题或标签。它会显示在TensorBoard界面中,帮助你识别这条曲线。 第二个参…

(35)远程识别(又称无人机识别)(二)

文章目录 前言 4 ArduRemoteID 5 终端用户数据的设置和使用 6 测试 7 为OEMs添加远程ID到ArduPilot系统的视频教程 前言 在一些国家&#xff0c;远程 ID 正在成为一项法律要求。以下是与 ArduPilot 兼容的设备列表。这里(here)有一个关于远程 ID 的很好解释和常见问题列表…

【数据结构】排序算法——Lesson2

Hi~&#xff01;这里是奋斗的小羊&#xff0c;很荣幸您能阅读我的文章&#xff0c;诚请评论指点&#xff0c;欢迎欢迎 ~~ &#x1f4a5;&#x1f4a5;个人主页&#xff1a;奋斗的小羊 &#x1f4a5;&#x1f4a5;所属专栏&#xff1a;C语言 &#x1f680;本系列文章为个人学习…

uni-app pinia搭建

1.新建store文件 新建index.js&#xff0c;代码&#xff1a; // import { // createPinia // } from pinia //const store createPinia() import * as Pinia from pinia const pinia Pinia.createPinia() export * from "./modules/user" export * from ".…

vscode 寻找全部分支的提交

vscode 寻找全部分支的提交 Git Graph

Python 机器学习求解 PDE 学习项目——PINN 求解二维 Poisson 方程

本文使用 TensorFlow 1.15 环境搭建深度神经网络&#xff08;PINN&#xff09;求解二维 Poisson 方程: 模型问题 − Δ u f in Ω , u g on Γ : ∂ Ω . \begin{align} -\Delta u & f \quad & \text{in } \Omega,\\ u & g \quad & \text{on } \Gamma:\p…

Proxmox8基于PC物理机/服务器安装,初始化,挂载磁盘,安装虚拟机

目录 安装文件 开始安装Proxmox 选择启动菜单&#xff0c;F11 后进入启动菜单选择 按需选择是否关闭RAID 选择对应的U盘 进入安装界面 进入安装启动过程 选择系统盘 设置相关信息 设置IP和开启root远程登录 设置dns 设置网卡ip 设置 ssh 远程登录 开机合并local-l…

Telegram曝零日漏洞,可伪装成视频攻击安卓用户

ESET Research在一个地下论坛上发现了一个针对Android Telegram的零日漏洞广告。 ESET将该漏洞命名为“EvilVideo”&#xff0c;并将其报告给Telegram&#xff0c;Telegram于7月11日更新了该应用程序。 EvilVideo允许攻击者发送恶意的有效载荷&#xff0c;这些载荷以视频文件…

计算机网络-配置双机三层互联(静态路由方式)

目录 交换机工作原理路由器工作原理路由信息表组成部分路由器发决策 ARP工作原理配置双机三层互联&#xff08;静态路由方式&#xff09; 交换机工作原理 MAC自学习过程 初始状态&#xff1a; 刚启动的交换机的MAC地址表是空的。 学习过程&#xff1a; 当交换机收到一个数据帧…

论文阅读——Integrated Diffusive Antenna Array of Low Backscattering

文章目录 摘要一、背景介绍二、天线结构A. 缝隙天线B. 低频扩散单元C. 高频扩散单元D. 集成设计 三、验证总结 论文来源&#xff1a;https://ieeexplore.ieee.org/document/10309141 摘要 文章提出了一种低雷达散射截面&#xff08;RCS&#xff09;的扩散天线阵列。 作为示例…