浮点数原理与`BigDecimal`实践应用

浮点数原理与BigDecimal实践应用

问题引入:

image-20240625092730585

浮点数

浮点数如何表示数字?

浮点数采用科学计数法表示一个数字,具体格式为:
V = ( − 1 ) S ∗ M ∗ R E V = (-1)^S * M * R^E V=(1)SMRE

  • S:符号位,取值 0 或 1,决定一个数字的符号,0 表示正,1 表示负
  • M:尾数,用小数表示
  • R:基数,表示二进制数 R 就是 2
  • E:指数,用整数表示

如何使用以上方法表示:25.125(D)

  1. 转化为十进制
    • 整数部分: 25(D) = 11001(B)
    • 小数部分: 0.125(D) = 0.001(B)
  2. 用二进制科学计数法表示
    • 25.125(D) = 11001.001(B) = 1.1001001 * 2 ^4(B)

假如填充到32bit中,且假定符号位S占1bit、指数E占10bit、尾数M占21bit,那么就是这样的:

0 0000000100 100100100000000000000

指数和尾数分配的位数不同,会产生以下情况:

  1. 指数位越多,尾数位则越少,其表示的范围越大,但精度就会变差,反之,指数位越少,尾数位则越多,表示的范围越小,但精度就会变好
  2. 一个数字的浮点数格式,会因为定义的规则不同,得到的结果也不同,表示的范围和精度也有差异

早期人们提出浮点数定义时,就是这样的情况,当时有很多计算机厂商,例如IBM、微软等,每个计算机厂商会定义自己的浮点数规则,不同厂商对同一个数表示出的浮点数是不一样的。

浮点数标准与标准浮点数的表示

1985年,IEEE 组织推出了浮点数标准,这个标准统一了浮点数的表示形式,并提供了 2 种浮点格式:

  • 单精度浮点数 float:32 位,符号位 S 占 1 bit,指数 E 占 8 bit,尾数 M 占 23 bit
  • 双精度浮点数 float:64 位,符号位 S 占 1 bit,指数 E 占 11 bit,尾数 M 占 52 bit

为了使其表示的数字范围、精度最大化,浮点数标准还对指数和尾数进行了规定:

  1. 尾数 M 的第一位总是 1(因为 1 <= M < 2),因此这个 1 可以省略不写,它是个隐藏位,这样单精度 23 位尾数可以表示了 24 位有效数字,双精度 52 位尾数可以表示 53 位有效数字
  2. 指数 E 是个无符号整数,表示 float 时,一共占 8 bit,所以它的取值范围为 0 ~ 255。但因为指数可以是负的,所以规定在存入 E 时在它原本的值加上一个中间数 127,这样 E 的取值范围为 -127 ~ 128(实际为-126 ~ 127)。表示 double 时,一共占 11 bit,存入 E 时加上中间数 1023,这样取值范围为 -1023 ~ 1024。

除了规定尾数和指数位,还做了以下规定:

  • 指数 E 非全 0 且非全 1:规格化数字,按上面的规则正常计算
  • 指数 E 全 0,尾数非 0:非规格化数,尾数隐藏位不再是 1,而是 0(M = 0.xxxxx),这样可以表示 0 和很小的数
  • 指数 E 全 1,尾数全 0:正无穷大/负无穷大(正负取决于 S 符号位)
  • 指数 E 全 1,尾数非 0:NaN(Not a Number)

img

上述案例25.125转换为标准的float浮点数:

  1. 转化为十进制
    • 整数部分: 25(D) = 11001(B)
    • 小数部分: 0.125(D) = 0.001(B)
  2. 用二进制科学计数法表示
    • 25.125(D) = 11001.001(B) = 1.1001001 * 2 ^4(B)
  3. 标准转换
    • 尾数 M = 1.001001,去掉1后为 001001
    • 指数 E = 4 + 127(中间数) = 131(D) = 10000011

0 10000011 00100100000000000000

浮点数为什么有精度丢失?

如果我们现在想用浮点数表示 0.2,它的结果会是多少呢?

0.2 转换为二进制数的过程为,不断乘以 2,直到不存在小数为止,在这个计算过程中,得到的整数部分从上到下排列就是二进制的结果。

0.2 * 2 = 0.4 -> 0
0.4 * 2 = 0.8 -> 0
0.8 * 2 = 1.6 -> 1
0.6 * 2 = 1.2 -> 1
0.2 * 2 = 0.4 -> 0(发生循环)
...

所以 0.2(D) = 0.00110…(B)

因为十进制的 0.2 无法精确转换成二进制小数,而计算机在表示一个数字时,宽度是有限的,无限循环的小数存储在计算机时,只能被截断,所以就会导致小数精度发生损失的情况。

浮点数的范围和精度有多大?

范围

float能表示的最大二进制为: + 1.111111...1 * 2^127

1.111111...1 ≈ 2

因此float能表示的最大数为2^128 = 3.4 * 10^38,即 float 的表示范围为:-3.4 * 10^38 ~ 3.4 * 10 ^38

同样的方式算出来double的范围为: -1.79 * 10^308 ~ +1.79 * 10^308

精度

float 能表示的最小二进制数为 0.0000….1(小数点后22个0,1个1),用十进制数表示就是 1/2^23 ≈ 1.19 * 10^-7

double 的最小精度为:0.0000…1(51个0,1个1),用十进制表示就是 1/2^52 ≈ 2.22 * 10^-16

BigDecimal

BigDecimal 可以实现对浮点数的运算,不会造成精度丢失。

通常情况下,大部分需要浮点数精确运算结果的业务场景(比如涉及到钱的场景)都是通过 BigDecimal 来做的。

BigDecimal原理简介

BigDecimal 类通过两个主要字段来表示一个数值:

  • int scale:表示小数位的数量。
  • BigInteger intVal:一个 BigInteger 对象,表示数值的非标度部分,即实际的数值(十进制)。

例如,数值 123.45 可以表示为:

  • intVal = 12345
  • scale = 2

使用BigDecimal的常见的坑

坑1:创建BigDecimal对象

常见的构造函数:

// 创建一个具有参数所指定整数值的对象
public BigDecimal(int) 
    
// 创建一个具有参数所指定双精度值的对象
public BigDecimal(double) 
    
// 创建一个具有参数所指定长整数值的对象
public BigDecimal(long) 
    
// 创建一个具有参数所指定以字符串表示的数值的对象
public BigDecimal(String) 

使用实例:

BigDecimal bd1 = new BigDecimal(0.01);
BigDecimal bd2 = BigDecimal.valueOf(0.01);
BigDecimal bd3 = new BigDecimal("0.01");
BigDecimal bd4 = new BigDecimal(Double.toString(0.01));
System.out.println("bd1 = " + bd1);
System.out.println("bd2 = " + bd2);
System.out.println("bd3 = " + bd3);
System.out.println("bd4 = " + bd4);

输出结果:

bd1 = 0.01000000000000000020816681711721685132943093776702880859375
bd2 = 0.01
bd3 = 0.01
bd4 = 0.01

阿里巴巴java开发手册:

在这里插入图片描述

坑2:使用divide方法方法结果为无限循环小数

使用实例:

// 含税金额
BigDecimal inclusiveTaxAmount = new BigDecimal("1000");
// 税率
BigDecimal taxRate = new BigDecimal("0.13");
// 不含税金额 = 含税金额 / (1+税率)
BigDecimal exclusiveTaxAmount = inclusiveTaxAmount.divide(BigDecimal.ONE.add(taxRate));
System.out.println(exclusiveTaxAmount);

输出结果:

Exception in thread "main" java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.

官网文档:

If the quotient has a nonterminating decimal expansion and the operation is specified to return an exact result, an ArithmeticException is thrown. Otherwise, the exact result of the division is returned, as done for other operations.

大意是,如果除法的商的结果是一个无限小数但是我们期望返回精确的结果,那程序就会抛出异常。

解决方案:

// 不含税金额 = 含税金额 / (1+税率)
BigDecimal exclusiveTaxAmount = inclusiveTaxAmount.divide(BigDecimal.ONE.add(taxRate), 2, RoundingMode.HALF_UP);
坑3:保留小数位数

BigDecimal保留小数位数,主要用setScale方法:

public BigDecimal setScale(int newScale)

public BigDecimal setScale(int newScale, RoundingMode roundingMode)

RoundingMode 参数说明:

ROUND_CEILING      //向正无穷方向舍入
ROUND_DOWN         //向零方向舍入
ROUND_FLOOR        //向负无穷方向舍入
ROUND_HALF_DOWN    //向(距离)最近的一边舍入,除非两边(的距离)是相等,如果是这样,向下舍入,例如1.55 保留一位小数结果为1.5
ROUND_HALF_EVEN    //向(距离)最近的一边舍入,除非两边(的距离)是相等,如果是这样,如果保留位数是奇数,使用ROUND_HALF_UP,如果是偶数,使用ROUND_HALF_DOWN
ROUND_HALF_UP      //向(距离)最近的一边舍入,除非两边(的距离)是相等,如果是这样,向上舍入, 1.55保留一位小数结果为1.6
ROUND_UNNECESSARY  //计算结果是精确的,不需要舍入模式
ROUND_UP           //向远离0的方向舍入

使用示例:

BigDecimal b = new BigDecimal("1.6666");
System.out.println("result b:" + b.setScale(2, BigDecimal.ROUND_HALF_UP));
System.out.println("result b:" + b.setScale(2)); 

输出结果:

result b:1.67
Exception in thread "main" java.lang.ArithmeticException: Rounding necessary
坑4:BigDecimal等值比较

BigDecimal提供了equalscompareTo两个方法可以进行比较;

使用示例:

BigDecimal bd1 = new BigDecimal("2.0");
BigDecimal bd2 = new BigDecimal("2.00");

System.out.println(bd1.equals(bd2));
System.out.println(bd1.compareTo(bd2));

输出结果:

false
true

分析原因:

BigDecimal中equals方法的实现会比较两个数字的精度,而compareTo方法则只会比较数值的大小。

阿里巴巴Java开发手册:

在这里插入图片描述

坑5:使用BigDecimal进行除法计算时被除数不能为0

使用示例:

BigDecimal number1 = new BigDecimal("88.66");
BigDecimal number2 = BigDecimal.ZERO;

BigDecimal number3 = number1.divide(number2);
System.out.println("number1 divide number2 = " + number3);

输出结果:

Exception in thread "main" java.lang.ArithmeticException: Division by zero
坑6: BigDecimal 不可变

BigDecimalString 一样具有对象不可变行,一旦赋值就不会再变,即便做加减乘除运算

BigDecimal count = new BigDecimal("3.1415");
count.add(new BigDecimal("0.1645"));

System.out.println("count:" + count); 
System.out.println("result:" + count.add(new BigDecimal("0.1645"))); 

输出结果:

count:3.1415
result:3.3060
坑7: 字符串输出
BigDecimal d = BigDecimal.valueOf(12334535345456700.12345634534534578901);
String out = d.toString();
System.out.println(out); 

输出结果:

1.23345353454567E+16

BigDecimal为转化为String提供了三个方法:

// 有必要时使用科学计数法
String toString();   
// 不使用科学计数法
String toPlainString(); 
// 工程计算中经常使用的记录数字的方法,与科学计数法类似,但要求10的幂必须是3的倍数
String toEngineeringString();  

补充

小数转化为二进制的方法

将小数部分转化为二进制的方法是通过乘以2并记录每次乘法的整数部分,直到小数部分变为0或达到所需的精度。以下是具体步骤:

转换步骤
  1. 乘以2:将小数部分乘以2。
  2. 取整:记录乘以2后的整数部分(0或1),这就是二进制表示的下一位。
  3. 取小数:将乘以2后的结果减去整数部分,得到新的小数部分。
  4. 重复步骤:重复以上步骤,直到小数部分变为0或者达到所需的精度。
示例:将十进制小数0.625转换为二进制
  1. 初始值0.625
  2. 第一步
    • 0.625 × 2 = 1.25
    • 整数部分:1
    • 小数部分:0.25
  3. 第二步
    • 0.25 × 2 = 0.5
    • 整数部分:0
    • 小数部分:0.5
  4. 第三步
    • 0.5 × 2 = 1.0
    • 整数部分:1
    • 小数部分:0.0(结束)

将记录的整数部分按顺序排列得到二进制表示:

  1. 625(D)= 0.101(B)
示例:将十进制小数0.1转换为二进制
  1. 初始值0.1
  2. 第一步
    • 0.1 × 2 = 0.2
    • 整数部分:0
    • 小数部分:0.2
  3. 第二步
    • 0.2 × 2 = 0.4
    • 整数部分:0
    • 小数部分:0.4
  4. 第三步
    • 0.4 × 2 = 0.8
    • 整数部分:0
    • 小数部分:0.8
  5. 第四步
    • 0.8 × 2 = 1.6
    • 整数部分:1
    • 小数部分:0.6
  6. 第五步
    • 0.6 × 2 = 1.2
    • 整数部分:1
    • 小数部分:0.2
  7. 第六步
    • 0.2 × 2 = 0.4
    • 整数部分:0
    • 小数部分:0.4
  8. (重复上述步骤)

小数0.1的二进制表示是一个循环小数:
0.1(D)= 0.0001100110011...(B)

数学原理这里不再说明,若有疑问,请自行百度。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

第一百二十六节 Java面向对象设计 - Java枚举类

Java面向对象设计 - Java枚举类 枚举类型的超类 编译枚举类型时&#xff0c;编译器会创建一个类。 枚举类型可以具有构造函数&#xff0c;字段和方法。枚举类型仅在编译器生成的代码中实例化。 每个枚举类型都隐式地扩展java.lang.Enum类。 Enum类中定义的所有方法都可以与…

网络爬虫中Xpath的使用方法

正则表达式虽然可以处理包含了诸如 HTML 或 XML 内容的字符串&#xff0c;但只能根据文本的 特征匹配字符串&#xff0c;而忽略字符串所包含的内容的真实格式。为了解决这个问题&#xff0c;Python 引入 XPath 以及支持 XPath 的第三方库 lxml&#xff0c;专门对 XML 或 HTML 格…

基于python的随机森林多分类模型

1.随机森林多分类模型 1.1 基本原理 随机森林&#xff08;Random Forest&#xff09;是一种基于决策树的集成学习方法&#xff0c;它通过将多个决策树进行组合&#xff0c;以投票或平均的方式得到最终的预测结果。在多分类问题中&#xff0c;随机森林通过构建多个决策树&#…

Gartner发布开发敏捷网络安全计划指南:安全计划是一个在快速变化的环境中运作的复杂生态系统

随着企业数字化程度的提高&#xff0c;它们面临的网络安全风险和威胁只会增加。这项研究有助于高管领导了解敏捷网络安全计划的重要性&#xff0c;以及如何与安全和风险利益相关者合作&#xff0c;为下一次重大破坏做好准备。 主要发现 新冠肺炎疫情、地区冲突、全球政治紧张局…

cropperjs 裁剪/框选图片

1.效果 2.使用组件 <!-- 父级 --><Cropper ref"cropperRef" :imgUrl"url" searchImg"searchImg"></Cropper>3.封装组件 <template><el-dialog :title"title" :visible.sync"dialogVisible" wi…

游戏服务器研究二:大世界的 scale 问题

这是一个非常陈旧的话题了&#xff0c;没什么新鲜的&#xff0c;但本人对 scale 比较感兴趣&#xff0c;所以研究得比较多。 本文不会探讨 MMO 类的网游提升单服承载人数有没有意义&#xff0c;只单纯讨论技术上如何实现。 像 moba、fps、棋牌、体育竞技等 “开房间类型的游戏…

前端:HTML、CSS、JavaScript 代码注释 / 注释与代码规范

一、HTML 行内注释 HTML注释是在HTML代码中添加说明和解释的一种方法&#xff0c;这些注释不会被浏览器渲染或显示在页面上&#xff0c;而是被浏览器忽略。HTML注释对于代码的可读性、可维护性和团队协作非常重要。 1.1、HTML注释的语法 HTML注释的语法是以<!--开始&…

中小学劳技课程开展创意木工 传承非遗木工魅力

学生劳技课程&#xff0c;全称劳动技术课程&#xff0c;是一门旨在通过实践活动培养学生的劳动技能、创新思维、实践能力和社会责任感的基础教育课程。这门课程强调学生的参与和体验&#xff0c;让学生在动手实践中学习并掌握知识&#xff0c;提高解决问题的能力。 学生劳技课程…

大模型应用研发基础环境配置(Miniconda、Python、Jupyter Lab、Ollama等)

老牛同学之前使用的MacBook Pro电脑配置有点旧&#xff08;2015 年生产&#xff09;&#xff0c;跑大模型感觉有点吃力&#xff0c;操作起来有点卡顿&#xff0c;因此不得已捡起了尘封了快两年的MateBook Pro电脑&#xff08;老牛同学其实不太喜欢用 Windows 电脑做研发工作&am…

解码数智升级良方:中国一拖、中原传媒、神火股份等企业数字化实践分析

大模型、AI等技术的成熟以及政策法规的细化&#xff0c;数据资源的权属论证、合规确权、资产论证等环节逐渐走向实用性、价值化。 而伴随着“业财税数融”综合性数字化成为企业数字化转型的主流选择&#xff0c;财务部门的纽带属性被放大&#xff0c;财务数据的融合能力成为企业…

ABC234G Divide a Sequence 题解

题目来源 ABC234G 洛谷 Description 给定长度为 n n n 的序列 { a n } \{a_n\} {an​}。定义一种将 { a n } \{a_n\} {an​} 划分为若干段的方案的价值为每段的最大值减去最小值的差的乘积。求所有划分方案的价值的总和并对 998244353 998244353 998244353 取模。 1 ≤…

Vue3 使用 Vue Router 时,params 传参失效

前言&#xff1a; 在写项目的时候&#xff0c;使用了 vue-router 的 params 进行传参&#xff0c;但是在详情页面中一直获取不到参数。原因&#xff1a;Vue Router 在2022-8-22的那次更新后&#xff0c;使用这种方式在新页面上无法获取&#xff01; 正文&#xff1a; 在列表页进…

从零开始做题:老照片中的密码

老照片中的密码 1.题目 1.1 给出图片如下 1.2 给出如下提示 这张老照片中的人使用的是莫尔斯电报机&#xff0c;莫尔斯电报机分为莫尔斯人工电报机和莫尔斯自动电报机&#xff08;简称莫尔斯快机&#xff09;。莫尔斯人工电报机是一种最简单的电报机&#xff0c;由三个部分组…

【笔记】从零开始做一个精灵龙女-拆uv阶段

目录 先回顾一下拆uv的基础流程吧 肩部盔甲分UV示例 手环UV部分 腰带UV部分 其它也差不多&#xff0c;需要删掉一半的就先提前删掉一半&#xff0c;然后把不需要的被遮挡的面也删掉 龙角UV 胸甲UV 侧边碎发UV 马尾UV 脸部/耳朵UV 特殊情况&#xff1a;如果要删一半再…

kafka的命令行操作

kafka-topics.bat 该命令行和主题相关 kafka启动后&#xff0c;默认端口为9092,可修改 找到kafka_2.13-3.6.2\bin\windows目录下的kafka-topics.bat&#xff0c;用cmd执行 按下会有提示&#xff0c;REQURIED代表为必输项 创建topic 创建一个名为test的topic队列 kafka-t…

绘制图形

自学python如何成为大佬(目录):https://blog.csdn.net/weixin_67859959/article/details/139049996?spm1001.2014.3001.5501 在前3节的实例中&#xff0c;我们一直绘制的都是直线&#xff0c;实际上&#xff0c;海龟绘图还可以绘制其他形状的图形&#xff0c;如圆形、多边形等…

FineReport聚合报表与操作

一、报表类型 模板设计是 FineReport 学习过程中的主要难题所在&#xff0c;FineReport 模板设计主要包括普通报表、聚合报表、决策报表三种设计类型。 报表类型简介- FineReport帮助文档 - 全面的报表使用教程和学习资料 二、聚合报表 2-1 介绍 聚合报表指一个报表中包含多个…

STM32的SPI通信

1 SPI协议简介 SPI&#xff08;Serial Peripheral Interface&#xff09;协议是由摩托罗拉公司提出的通信协议&#xff0c;即串行外围设备接口&#xff0c;是一种高速全双工的通信总线。它被广泛地使用在ADC、LCD等设备与MCU间&#xff0c;使用于对通信速率要求较高的场合。 …

扩散模型 GLIDE:35 亿参数的情况下优于 120 亿参数的 DALL-E 模型

节前&#xff0c;我们星球组织了一场算法岗技术&面试讨论会&#xff0c;邀请了一些互联网大厂朋友、参加社招和校招面试的同学。 针对算法岗技术趋势、大模型落地项目经验分享、新手如何入门算法岗、该如何准备、面试常考点分享等热门话题进行了深入的讨论。 合集&#x…

LeetCode 算法:二叉树的层序遍历 c++

原题链接&#x1f517;&#xff1a;二叉树的层序遍历 难度&#xff1a;中等⭐️⭐️ 题目 给你二叉树的根节点 root &#xff0c;返回其节点值的 层序遍历 。 &#xff08;即逐层地&#xff0c;从左到右访问所有节点&#xff09;。 示例 1&#xff1a; 输入&#xff1a;roo…