编程进阶网编程进阶网
  • 基础组成体系
  • 程序编程原理
  • 异常和IO系统
  • 六大设计原则
  • 设计模式导读
  • 创建型设计模式
  • 结构型设计模式
  • 行为型设计模式
  • 设计模式案例
  • 面向对象思想
  • 基础入门
  • 高级进阶
  • JVM虚拟机
  • 数据集合
  • Java面试题
  • C语言入门
  • C综合案例
  • C标准库
  • C语言专栏
  • C++入门
  • C++综合案例
  • C++专栏
  • HTML
  • CSS
  • JavaScript
  • 前端专栏
  • Swift
  • iOS入门
  • 基础入门
  • 开源库解读
  • 性能优化
  • Framework
  • 方案设计
  • 媒体音视频
  • 硬件开发
  • Groovy
  • 常用工具
  • 大厂面试题
  • 综合案例
  • 网络底层
  • Https
  • 网络请求
  • 故障排查
  • 专栏
  • 数组
  • 链表
  • 栈
  • 队列
  • 树
  • 递归
  • 哈希
  • 排序
  • 查找
  • 字符串
  • 其他
  • Bash脚本
  • Linux入门
  • 嵌入式开发
  • 代码规范
  • Markdown
  • 开发理论
  • 开发工具
  • Git管理
  • 百宝箱
  • 开源协议
  • 技术招聘
  • 测试经验
  • 职场提升
  • 技术模版
  • 关于我
  • 目标清单
  • 学习框架
  • 育儿经验
  • 我的专栏
  • 底层能力
  • 读书心得
  • 随笔笔记
  • 职场思考
  • 中华历史
  • 经济学故事
  • 基础组成体系
  • 程序编程原理
  • 异常和IO系统
  • 六大设计原则
  • 设计模式导读
  • 创建型设计模式
  • 结构型设计模式
  • 行为型设计模式
  • 设计模式案例
  • 面向对象思想
  • 基础入门
  • 高级进阶
  • JVM虚拟机
  • 数据集合
  • Java面试题
  • C语言入门
  • C综合案例
  • C标准库
  • C语言专栏
  • C++入门
  • C++综合案例
  • C++专栏
  • HTML
  • CSS
  • JavaScript
  • 前端专栏
  • Swift
  • iOS入门
  • 基础入门
  • 开源库解读
  • 性能优化
  • Framework
  • 方案设计
  • 媒体音视频
  • 硬件开发
  • Groovy
  • 常用工具
  • 大厂面试题
  • 综合案例
  • 网络底层
  • Https
  • 网络请求
  • 故障排查
  • 专栏
  • 数组
  • 链表
  • 栈
  • 队列
  • 树
  • 递归
  • 哈希
  • 排序
  • 查找
  • 字符串
  • 其他
  • Bash脚本
  • Linux入门
  • 嵌入式开发
  • 代码规范
  • Markdown
  • 开发理论
  • 开发工具
  • Git管理
  • 百宝箱
  • 开源协议
  • 技术招聘
  • 测试经验
  • 职场提升
  • 技术模版
  • 关于我
  • 目标清单
  • 学习框架
  • 育儿经验
  • 我的专栏
  • 底层能力
  • 读书心得
  • 随笔笔记
  • 职场思考
  • 中华历史
  • 经济学故事
  • 1.1String深入理解原理
  • 1.2浮点型数据深入研究
  • 1.3数据装箱和拆箱原理
  • 1.4泛型由来和设计思想
  • 1.5加密和解密设计和原理
  • 2.1面向对象设计思想
  • 2.2抽象类和接口设计
  • 2.3封装和继承设计思想
  • 2.4复用和组合设计思想
  • 2.5对象和引用设计思想
  • 3.1IO流设计思想和原理
  • 3.2为何设计序列化数据
  • 3.3各种拷贝数据比较
  • 3.4高效文件读写的原理
  • 4.1反射性能探索和优化
  • 4.2为何要设计注解思想
  • 4.3动态代理的设计思想
  • 4.4SPI机制设计的思想
  • 4.5异常设计和捕获原理
  • 4.6虚拟机如何处理异常
  • 4.7四种引用设计思想
  • 5.1线程的前世今生探索
  • 5.2线程通信的设计思想
  • 5.3线程监控和Debug设计
  • 5.4线程和JVM之间联系
  • 5.5线程池使用技巧介绍
  • 5.6线程池设计核心原理
  • 5.7线程如何最大优化
  • 6.1多线程并发经典案例
  • 6.2并发安全前世今生
  • 6.3线程安全如何保证
  • 6.4变量的线程安全探索
  • 6.5并发上下文切换原理
  • 6.6理解CAS设计和由来
  • 6.7协程设计思想和原理
  • 6.8事物并发模型解读
  • 6.9并发设计模型研究
  • 6.10并发编程数据一致性
  • 6.11锁问题的定位和修复
  • 6.12多线程如何性能调优
  • 7.1类的加载过程和原理
  • 7.2对象布局设计的原理
  • 7.3双亲委派机制设计思想
  • 7.5代码攻击和安全防护
  • 7.6设计动态生成Java类

1.2浮点型数据深入研究

目录介绍

  • 01.浮点型数据对比分析
    • 1.1 首先看个案例1
    • 1.2 对结果分析
    • 1.3 默认double思考
  • 02.浮点型数据精度丢失
    • 2.1 看一个案例2
    • 2.2 案例分析一下
    • 2.4 什么精度丢失
    • 2.5 精度为何会丢失
    • 2.6 浮点数如何判断
  • 03.计算机设计浮点由来
    • 3.1 用比特表示数
    • 3.2 定点数的表示
    • 3.3 浮点数的表示
    • 3.4 浮点型原理设计
    • 3.5 浮点型编码格式
    • 3.6 符号位设计思想
    • 3.7 阶码位设计思想
    • 3.8 尾数位设计思想
  • 04.浮点数二进制转化
    • 4.1 浮点数二进制表示
    • 4.2 浮点数据转化十进制
    • 4.3 浮点数规格化表示
    • 4.4 浮点数加法和精度丢失
  • 05.回归到开始问题
    • 5.1 浮点数解析代码
    • 5.2 案例1结果分析
    • 5.3 案例2结果分析
  • 06.BigDecimal设计
    • 6.1 BigDecimal介绍
    • 6.2 BigDecimal设计思想
    • 6.3 BigDecimal原理

01.浮点数据对比

1.1 首先看个案例

  • 先看一个案例,思考一下是否妥当
    • 在此类问题中,编码时需要注意float基本数据类型需要添加后缀f,以及它的包装类Float的后缀F,不写后缀会默认为double类型。
      float a = 2.0000001F;
      float b = 2.0000001F;
      System.out.println(a == b);                 // 输出true
      System.out.println(0.1f + 0.2f == 0.3f);    // 输出true
      System.out.println(0.1 + 0.2 == 0.3);       // 输出false
      System.out.println(0.3f + 0.6f == 0.9f);    // 输出false
      System.out.println(0.3 + 0.6 == 0.9);       // 输出false
      System.out.println(0.6 + 0.6 == 1.2);       // 输出true
      System.out.println(0.2 + 0.2 == 0.4);       // 输出true
  • 首先说一下测试结果
    //打印结果
    //true
    //true
    //false
    //false
    //false
    //true
    //true

1.2 对结果分析

  • 开始看到这个结果是不是直接懵了,先且不说不写后缀默认double的问题
    • System.out.println(0.1f + 0.2f == 0.3f); // 输出true
    • System.out.println(0.3f + 0.6f == 0.9f); // 输出false
    • 一个true一个false就直接懵逼了。那为什么会出现这样的情况,思考一下?要想搞清楚,我们就必须搞清楚计算机是如何存储浮点型数据的原理。

1.3 默认double思考

  • 默认double数据对比思考
    • System.out.println(0.1 + 0.2 == 0.3); // 输出false
    • System.out.println(0.3 + 0.6 == 0.9); // 输出false
    • System.out.println(0.6 + 0.6 == 1.2); // 输出true
    • System.out.println(0.2 + 0.2 == 0.4); // 输出true
  • 通过这个案例可知
    • 默认double类型,为何第一个和第二个案例返回结果是false,为何第三个第四个返回结果是true。

02.浮点型数据精度丢失

2.1 看一个案例2

  • 题目
    float f = 1.4f;double d = 1.4d; 与 float f = 1.5f;double d = 1.5d; 是否为true
  • 测试代码
    float f = 1.4f;
    double d = 1.4d;
    float f1 = 1.5f;
    double d1 = 1.5d;
    
    System.out.println(f);              //1.4
    System.out.println((double)f);      //1.399999976158142
    System.out.println(d);              //1.4
    System.out.println(f - d);          //-2.3841857821338408E-8
    System.out.println(f == d);         //false
    
    System.out.println("--------");
    System.out.println(f1);             //1.5
    System.out.println((double) f1);    //1.5
    System.out.println(d1);             //1.5
    System.out.println(f1 - d1);        //0.0
    System.out.println(f1 == d1);       //true

2.2 案例分析一下

  • 思考一下为何1.4f转化为double数据后,结果比原值要小。
    • System.out.println((double)f); //1.399999976158142
  • 思考一下为何1.5f转化为double数据后,结果和原值一样。
    • System.out.println((double) f1); //1.5

2.4 什么精度丢失

  • 十进制小数如何转化为二进制数?算法是乘以2直到没有了小数为止。举个例子,0.3表示成二进制数
    0.3*2=0.6   取整数部分  0
    
    0.6*2=1.2   取整数部分  1
    
    0.2(1.2的小数部分)*2=0.4   取整数部分 0
    
    0.4*2=0.8   取整数部分  0
    
    0.8*2=1.6    取整数部分 1
    
    0.6*2=1.2   取整数部分  1
    
    .........     0.3二进制表示为(从上往下):0100110011......
  • 上面的计算过程循环了,也就是说2永远不可能消灭小数部分,这样算法将无限下去。
    • 很显然,小数的二进制表示有时是不可能精确的。而0.5能用二进制数精确表示0.52=1.0时能取整数部分。
    • 其实道理很简单,十进制系统中能不能准确表示出1/3呢?同样二进制系统也无法准确表示1/10。这也就解释了为什么浮点型减法出现了"减不尽"的精度丢失问题。

2.5 精度为何会丢失

  • 精度丢失主要有两个原因:
    • 有限的表示范围:浮点数的表示范围是有限的,无法准确表示所有的实数。当一个实数超出浮点数的表示范围时,它会被近似表示为最接近的可表示值。这种近似导致了精度的丢失。
    • 二进制表示:浮点数在计算机中以二进制形式表示。某些实数在十进制中是无限循环的,但在二进制中无法精确表示。这会导致一些实数在转换为浮点数时产生舍入误差,从而导致精度丢失。
  • 为何精度丢失
    • 在计算机中,一切皆为整数在算术类型上,又分为整数和浮点数,浮点数是由 符号位、有效数字、指数位这些整数共同构成的。
    • 但是计算机又不是使用人类的10进制,而是使用的2进制进行(指数)存储,所以理所当然的会有精度丢失。
    • 十进制。10进制中的1/3这种除法我们想表示的时候只能用0.33333....来表示,那么当我们显示的位数是固定的时候就存在精度问题,1/3!=0.33,也不等于0.333333333。假设小数点之后我们只能写8位或者16位那么久丢失了精度。
    • 二进制。所以在二进制中精度丢失也是一个道理。毕竟这种无限循环的事情,计算器不可能开辟一个无限大的空间去给你存储吧!

2.6 浮点数如何判断

  • 浮点数判断需要注意,float和double的精度范围,超过范围的数字会被忽略
    • (1) 浮点数大小判断:如果没有等号关系在里面,也就必然一大一小,那么直接用 > 或者 <
    • (2) 浮点数相等判断:因为浮点数在内存中存放,可能无法精确的储存,所以同一个值,可能有不同的内存数据
  • 正确做法
    • 应该使用两个浮点数之间的差异的绝对值小于某个可以接受的值来判断判断它们是否相等
    private static final float EQUAL_FLOAT = 0.0000001f;
    if (Math.abs(a-b)> EQUAL_FLOAT && Math.abs(b-a)> EQUAL_FLOAT){
        
    }

03.计算机设计浮点由来

3.1 用比特表示数

  • 现在用的计算机通常用 16/32 个比特(bit)来表示一个数。那我问你,我们用 32 个比特,能够表示所有实数吗?
    • 答案很显然是不能。32 个比特,只能表示 2 的 32 次方个不同的数,差不多是 40 亿个。如果表示的数要超过这个数,就会有两个不同的数的二进制表示是一样的。
    • 那计算机可就会一筹莫展,不知道这个数到底是多少。 40 亿个数看似已经很多了,但是比起无限多的实数集合却只是沧海一粟。
    • 所以,这个时候,计算机的设计者们,就要面临一个问题了:我到底应该让这 40 亿个数映射到实数集合上的哪些数,在实际应用中才能最划得来呢?

3.2 定点数的表示

  • 有一个很直观的想法,就是我们用 4 个比特来表示 0~9 的整数,那么 32 个比特就可以表示 8 个这样的整数。
    0001    表示1
    0010    表示2
    0011    表示3
    0100    表示4
    0101    表示5
    1000    表示8
    1001    表示9
    • 然后我们把最右边的 2 个 0~9 的整数,当成小数部分;把左边 6 个 0~9 的整数,当成整数部分。
    • 这样,我们就可以用 32 个比特,来表示从 0 到 999999.99 这样 1 亿个实数了。
  • 种用二进制来表示十进制的编码方式,叫作BCD 编码(Binary-Coded Decimal)。
    • 其实它的运用非常广泛,最常用的是在超市、银行这样需要用小数记录金额的情况里。在超市里面,我们的小数最多也就到分。这样的表示方式,比较直观清楚,也满足了小数部分的计算。
  • 二进制表达数字有一些缺点
    • 第一,这样的表示方式有点“浪费”。本来 32 个比特我们可以表示 40 亿个不同的数,但是在 BCD 编码下,只能表示 1 亿个数,如果我们要精确到分的话,那么能够表示的最大金额也就是到 100 万。
    • 第二,这样的表示方式没办法同时表示很大的数字和很小的数字。有时候我们想要表示商品的金额,关心的是 9.99 这样小的数字;有时候,我们又要进行物理学的运算,需要表示光速,也就是 3×108 这样很大的数字。
    • 那么,我们有没有一个办法,既能够表示很小的数,又能表示很大的数呢?

3.3 浮点数的表示

  • 先来想一想。如果我们想在一张便签纸上,用一行来写一个十进制数,能够写下多大范围的数?
    • 因为我们要让人能够看清楚,所以字最小也有一个限制。你会发现一个和上面我们用 BCD 编码表示数一样的问题,就是纸张的宽度限制了我们能够表示的数的大小。
    • 如果宽度只放得下 8 个数字,那么我们还是只能写下最大到 99999999 这样的数字。
  • 在现实生活中,我们是怎么表示一个很大的数的呢?
    • 比如说,我们想要在一本科普书里,写一下宇宙内原子的数量,莫非是用一页纸,用好多行写下很多个 0 么?
    • 用科学计数法来表示这个数字。宇宙内的原子的数量,大概在 10 的 82 次方左右,我们就用1.0\times 10^{82} 这样的形式来表示这个数值,不需要写下 82 个 0。

3.4 浮点型原理设计

  • 浮点数的科学计数法的表示,有一个 IEEE 的标准,它定义了两个基本的格式。
    • 一个是用 32 比特表示单精度的浮点数,也就是我们常常说的 float 或者 float32 类型。
    • 另外一个是用 64 比特表示双精度的浮点数,也就是我们平时说的 double 或者 float64 类型。
    精度	      字节数	      正数取值范围	              负数取值范围
    单精度类型	   4	    1.4e-45至3.4e+38	     -3.4e+38至-1.4e-45
    双精度类型	   8	  4.9e-324至1.798e+308	   -1.798e+308至-4.9e-324
  • 因为浮点数无法表示零值,所以取值范围分为两个区间:正数区间和负数区间。以单精度类型float为例,它被分配了4个字节,总共 32 位,具体格式如下图所示
    • image
      image

3.5 浮点型编码格式

  • 指数称为“阶码”,有效数字称为“尾数”,所以用于存储符号、阶码、尾数的二进制位分别称为符号位、阶码位、尾数位,下面详细阐述三个部分的编码格式。
  • 计算机中的表示方法
    • 对于float来说,4个字节,32位,0-22位表示尾数,23-30(8位)表示指数,31位表示符号位。
    • 对于double来说,8个字节,64位,0-51表示尾数,52-62(11位)表示指数,63位最高位表示符号位。

3.6 符号位设计思想

  • 在最高二进制位上分配 位表示浮点数的符号,0表示正数,1表示负数

3.7 阶码位设计思想

  • 在符号位右侧分配位用来存储指数,IEEE754标准规定阶码位存储的是指数对应的移码,而不是指数的原码或补码。
    • 根据计算机组成原理中对移码的定义可知,移码是将一个真值在数轴正向平移个偏移量之后得到的,即[x]移=x+2(n-1)(n为的二进制位数,含符号位)。
    • 移码的几何意义是把真值映射到个正数域,其特点是可以直观地反映两个真值的大小,即移码大的真值也大。
    • 基于这个特点,对计算机来说用移码比较两个真值的大小非常简单,只要高位对齐后逐个比较即可,不用考虑负号的问题,这也是阶码会采用移码表示的原因所在。
  • 由于阶码实际存储的是指数的移码,所以指数与阶码之间的换算关系就是指数与它的移码之间的换算关系。
    • 假设指数的真值为e,阶码为E,则有E=e+(2(n-1)-1)其2(n-1)是IEEE754标准规定的偏移量,n=8是阶码的二进制位数。
  • 为什么偏移值为2(n-1)-1而不是2(n-1)呢?
    • 因为8个二进制位能表示指数的取值范围为[-128,127],现在将指数变成移码表示,即将区间[128,127]正向平移到正数域,区间里的每个数都需要加上128,从而得到阶码范围为[0,255]。
    • 由于计算机规定阶码全为0或全为1两种情况被当作特殊值处理(全0被认为是机器零,全1被认为是无穷大),去除这两个特殊值,阶码的取值范围变成了[1,254]。
    • 如果偏移量不变仍为128的话,则根据换算关系公式[x]阶+128得到指数的范围变成[-127,126],指数最大只能取到126,显然会缩小浮点数能表示的取值范围。
    • 所以IEEE754标准规定单精度的阶码偏移量为2(n-1)-1(即127),这样能表示的指数范围为[-126,127],指数最大值能取到127

3.8 尾数位设计思想

  • 最右侧分配连续的23位用来存储有效数字,IEEE754标准规定尾数以原码表示。
    • 正指数和有效数字的最大值决定了32位存储空间能够表示浮点数的十进制最大值。
    • 指数最大值为2127≈1.7×1038,而有效数字部分最大值是二进制的1.11···1(小数点后23),是个无限接近于2的数字,所以得到最大的十进制数为2×1.7×1038,再加上最左1位的符号,最终得到32位浮点数最大值为3.4e+38。

04.浮点数二进制转化

4.1 浮点数二进制表示

  • 首先来看,十进制的浮点数怎么表示成二进制。
    • 输入一个任意的十进制浮点数,背后都会对应一个二进制表示。比方说,我们输入了一个十进制浮点数 9.1。
    • 那么按照之前的讲解,在二进制里面,我们应该把它变成一个“符号位 s+ 指数位 e+ 有效位数 f”的组合。
  • 我们要做的,就是把这个数变成二进制。
    • 这个数的整数部分,变成一个二进制。整数的二进制表示采用“除以 2,然后看余数”的方式相比。这里的 9,换算之后就是 1001。
    • 对应的小数部分也换算成二进制。小数怎么换成二进制呢?小数部分转换成二进制是用一个相似的反方向操作,就是乘以 2,然后看看是否超过 1。如果超过 1,我们就记下 1,并把结果减去 1,进一步循环操作。
    • image
      image
    • 在这里,我们就会看到,0.1 其实变成了一个无限循环的二进制小数,0.000110011。这里的“0011”会无限循环下去。
    • 把整数部分和小数部分拼接在一起,9.1 这个十进制数就变成了 1001.000110011…这样一个二进制表示。
  • 这个二进制的科学计数法表示,我们就可以对应到了浮点数的格式里了。
    • 这里的符号位 s = 0,
    • 对应的有效位 f=001000110011…。因为 f 最长只有 23 位,那这里“0011”无限循环,最多到 23 位就截止了。于是,f=00100011001100110011 001。最后的一个“0011”循环中的最后一个“1”会被截断掉。
    • 对应的指数为 e,代表的应该是 3。因为指数位有正又有负,所以指数位在 127 之前代表负数,之后代表正数,那 3 其实对应的是加上 127 的偏移量 130,转化成二进制,就是 130,对应的就是指数位的二进制,表示出来就是 10000010。
    • image
      image
  • 把“s+e+f”拼在一起,就可以得到浮点数 9.1 的二进制表示了。最终得到的二进制表示就变成了:
    • 010000010 0010 0011001100110011 001

4.2 浮点数据转化十进制

  • 浮点数转化工具
    • 这里提供了直接交互式地设置符号位、指数位和有效位数的操作。你可以直观地看到,32 位浮点数每一个 bit 的变化,对应的有效位数、指数会变成什么样子以及最后的十进制的计算结果是怎样的。
  • 这个也解释了为什么,在上面的测试案例2中,0.3+0.6=0.899999。
    • 因为 0.3 转化成浮点数之后,并不是精确的 0.3 了,0.6 和 0.9 也是一样的,最后的计算会出现精度问题。

4.3 浮点数规格化表示

  • 来看一组表示: 0 111-1111-0 111-1111-1111-1111-1111-1111
    • 第一部分为符号位,值为0,表示正数
    • 第二部分为阶码位即指数,值为2的(254-127)次方=2的127次方≈1.7×10的38次方
    • 第三部分为尾数位即有效数字,值为1.11111111111111111111111(23个1)
    • 为了节约存储空间,将符合规格化尾数的首个1省略,所以尾数表面上是23位,却表示了24位二进制数。
  • 常用浮点数的规格化表示如下表所示:
    数值	    浮点数二进制表示	                          说明
    -16	    1100-0001-1000-0000-0000-0000-0000-0000	  第1位为负数,131-127=4,即2的4次方等于16,尾数部分为1.0
    16.35	0100-0001-1000-0010-1100-1100-1100-1101	  符号位正,绿色部分上同,尾数部分见说明①
    0.35	0011-1110-1011-0011-0011-0011-0011-0011	  此例的目的是说明16.35和0.35的尾数部分是不一样的
    1.0	    0011-1111-1000-0000-0000-0000-0000-0000	  127-127=0即2的0次=1,尾数部分为1.0
    0.9	    0011-1111-0110-0110-0110-0110-0110-0110	  126-127=-1即0.5②
    • ①尾数部分的有效数字为1.00000101100110011001101,将其转换成十进制值为1.021875,然后乘以 24 得到16.35000038。由此可见,计算机实际存储的值可能与真值是不一样的。
    • ②0.9不能用有限二进制位精确表示,所以1-0.9并不精确地等于0.1,实际结果是0.100000024

4.4 浮点数加法和精度丢失

05.回归到开始问题

5.1 浮点数解析代码

  • 使用代码辅助验证float的实际存储值,代码如下所示:
    public class FloatAnalysis{
    
        public static void main(String[] args) throws Exception {
            // 需要计算真实存储值的浮点数
            float floatValue1 = 16.35f;
            float floatValue2 = .35f;
            System.out.println(floatValue1 + "的真实存储值:" + rayFloatStudy(floatValue1));
            System.out.println("\n========我是一条华丽的分割线========\n");
            System.out.println(floatValue2 + "的真实存储值:" + rayFloatStudy(floatValue2));
        }
    
        /**
         * 解析浮点数组成
         *
         * @param floatValue 待解析的浮点数
         * @author LiaoYuXing-Ray 2024/1/4 10:01
         **/
        public static double rayFloatStudy(float floatValue) throws Exception {
            // 符号位(1位)
            byte[] symbol = new byte[1];
            // 阶码位(8位),指数部分
            byte[] exponent = new byte[8];
            // 尾数位(23位),有效数字
            byte[] number = new byte[23];
    
            String binaryRepresentation = floatToBinary(floatValue);
    
            System.out.println("浮点数\t" + floatValue);
            System.out.println("========开始解析每一位的含义========");
            System.out.println(binaryRepresentation);
            byte[] byteString = binaryRepresentation.getBytes();
            // float 4字节 1字节=8bit 所以32
            byte[] byteArray = new byte[32];
            // ASCII码中48是0 49是1
            for (int i = 0; i < byteString.length; i++) {
                if (byteString[i] == 48) {
                    byteArray[i] = 0;
                } else if (byteString[i] == 49) {
                    byteArray[i] = 1;
                } else {
                    throw new Exception("不应该出现的分支,理论上转为二进制只有0和1");
                }
            }
    
            /*
                以下是获取符号位、指数部分、有效数字的值,并输出
             */
            for (int i = 0; i < byteArray.length; i++) {
                if (i == 0) {
                    symbol[i] = byteArray[i];
                }
                if (i >= 1 && i <= 8) {
                    exponent[i - 1] = byteArray[i];
                }
                if (i > 8) {
                    number[i - 9] = byteArray[i];
                }
                // 以下为输出,可注释
                if (i != 0 && i % 4 == 0) {
                    System.out.print("-");
                }
                System.out.print(byteArray[i]);
            }
    
            System.out.print("\n符号位值(1位):" + Arrays.toString(symbol));
            if (symbol[0] == 0) {
                System.out.print("为正数");
            } else if (symbol[0] == 1) {
                System.out.print("为负数");
            } else {
                throw new Exception("不应该出现的分支,符号位理论上只有0和1两种情况");
            }
    
            System.out.print("\n指数部分(8位):[");
            for (byte b : exponent) {
                System.out.print(b);
            }
            System.out.print("]\t-> 转化十进制数:[" + binaryToDecimal(exponent) + "]");
    
            System.out.print("\n有效数字(23位):[");
            for (byte b : number) {
                System.out.print(b);
            }
            System.out.print("]");
    
            System.out.println("\n=======还原float的真实存储值=======");
    
            // 指数部分的十进制数值
            int exponentOfDecimalNumber = binaryToDecimal(exponent);
            /*
                以 IEEE754 标准规定,单精度的阶码偏移量为 2^(n-1)-1 (即127),这样能表示的指数范围为 [-126,127]
             */
            // 阶码偏移量
            int offsetExponent = exponentOfDecimalNumber - 127;
            System.out.println("阶码偏移量=指数部分的十进制数值[" + exponentOfDecimalNumber + "]- [2^(n-1)-1 (即127)]=" + offsetExponent);
    
            // 指数值。此处double是因为Math.pow方法的参数为double类型
            double integerBitsBaseValue;
            if (offsetExponent >= 0) {
                integerBitsBaseValue = 1 << offsetExponent;
            } else {
                integerBitsBaseValue = Math.pow(2D, offsetExponent);
            }
            System.out.println("指数值=2^阶码偏移量[" + offsetExponent + "]=" + integerBitsBaseValue);
    
            // 整数部分(小数点左边的部分)即整数位(基础值)
            double integerBits = integerBitsBaseValue;
    
            // 有效位数转化为10进制数
            double tempCount;
            if (offsetExponent >= 0) {
                tempCount = 1D;
                // 如果偏移量大于等于0,整数部分为0
                integerBits = 0;
                System.out.println("整数部分(小数点左边的部分)即整数位(基础值)=" + integerBits);
                System.out.println("尾数23位实际为1.xxx,尾数为有效数字加上1.0");
                System.out.print("所以有效位数的二进制表示为[1.");
    
            } else {
                tempCount = 0D;
                System.out.println("整数部分(小数点左边的部分)即整数位(基础值):" + integerBits);
                System.out.println("尾数23位部分为0.xxx,尾数为有效数字加上0.0");
                System.out.print("所以有效位数的二进制表示为[0.");
            }
    
            /*
                此处计算小数二进制转化为十进制,比如0.01(2)=0*2^(-1)+1*2^(-2)=0.25(10)
             */
            for (int i = 0; i < number.length; i++) {
                if (number[i] == 1) {
                    // 将2的负(i+1)次方累加
                    tempCount += Math.pow(2, -(i + 1));
                }
                System.out.print(number[i]);
            }
            System.out.println("] -> 有效位数10进制数的值" + tempCount);
    
            // 小数有效值
            double decimalEffectiveValue = tempCount * integerBitsBaseValue;
            System.out.println("(有效位数10进制数的值" + tempCount + ")*(指数值" + integerBitsBaseValue + ")=小数有效值:" + decimalEffectiveValue);
            double result = decimalEffectiveValue + integerBits;
            if (byteArray[0] == 0) {
                System.err.println(floatValue + "为正数,小数有效值[" + decimalEffectiveValue + "]加上整数部分[" + integerBits + "],最终结果:" + result);
            } else {
                System.err.println(floatValue + "为负数结果需要*(-1),小数有效值[" + decimalEffectiveValue + "]加上整数部分[" + integerBits + "],最终结果:" + (result * -1));
            }
    
            // 休眠是因为防止err语句输出顺序混乱
            try {
                TimeUnit.MILLISECONDS.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return result;
        }
    
        /**
         * 长度为8的byte[]转换为对应的十进制数
         *
         * @param exponentBytes 长度为8的byte[]
         * @return int
         * @author LiaoYuXing-Ray 2024/1/4 10:01
         **/
        private static int binaryToDecimal(byte[] exponentBytes) {
            int decimal = 0;
            for (int i = 0; i < exponentBytes.length; i++) {
                decimal += (exponentBytes[i] & 0xFF) * Math.pow(2, exponentBytes.length - 1 - i);
            }
            return decimal;
        }
    
        /**
         * 浮点型转化为bit,字符串输出,会自动补零
         *
         * @param value 浮点数
         * @return java.lang.String
         * @author LiaoYuXing-Ray 2024/1/4 10:01
         **/
        private static String floatToBinary(float value) {
            int intBits = Float.floatToIntBits(value);
            return String.format("%32s", Integer.toBinaryString(intBits)).replace(' ', '0');
        }
    }
  • 针对打印结果如下所示
    浮点数	16.35
    ========开始解析每一位的含义========
    01000001100000101100110011001101
    0100-0001-1000-0010-1100-1100-1100-1101
    符号位值(1位):[0]为正数
    指数部分(8位):[10000011]	-> 转化十进制数:[131]
    有效数字(23位):[00000101100110011001101]
    =======还原float的真实存储值=======
    阶码偏移量=指数部分的十进制数值[131]- [2^(n-1)-1 (即127)]=4
    指数值=2^阶码偏移量[4]=16.0
    整数部分(小数点左边的部分)即整数位(基础值)=0.0
    尾数23位实际为1.xxx,尾数为有效数字加上1.0
    所以有效位数的二进制表示为[1.00000101100110011001101] -> 有效位数10进制数的值1.021875023841858
    (有效位数10进制数的值1.021875023841858)*(指数值16.0)=小数有效值:16.350000381469727
    16.35为正数,小数有效值[16.350000381469727]加上整数部分[0.0],最终结果:16.350000381469727
    16.35的真实存储值:16.350000381469727
    
    
    浮点数	0.35
    ========开始解析每一位的含义========
    00111110101100110011001100110011
    0011-1110-1011-0011-0011-0011-0011-0011
    符号位值(1位):[0]为正数
    指数部分(8位):[01111101]	-> 转化十进制数:[125]
    有效数字(23位):[01100110011001100110011]
    =======还原float的真实存储值=======
    阶码偏移量=指数部分的十进制数值[125]- [2^(n-1)-1 (即127)]=-2
    指数值=2^阶码偏移量[-2]=0.25
    整数部分(小数点左边的部分)即整数位(基础值):0.25
    尾数23位部分为0.xxx,尾数为有效数字加上0.0
    所以有效位数的二进制表示为[0.01100110011001100110011] -> 有效位数10进制数的值0.3999999761581421
    (有效位数10进制数的值0.3999999761581421)*(指数值0.25)=小数有效值:0.09999999403953552
    0.35为正数,小数有效值[0.09999999403953552]加上整数部分[0.25],最终结果:0.3499999940395355
    0.35的真实存储值:0.3499999940395355

5.2 案例1结果分析

  • 通过上面的代码案例,拿到浮点数最终在计算机中的结果
    double f1 = FloatAnalysis.rayFloatStudy(0.1f);
    double f2 = FloatAnalysis.rayFloatStudy(0.2f);
    double f3 = FloatAnalysis.rayFloatStudy(0.3f);
    System.out.println(f1 + "\t= 0.1f");          // 0.10000000149011612	= 0.1f
    System.out.println(f2 + "\t= 0.2f");          // 0.20000000298023224	= 0.2f
    System.out.println(f3 + "\t= 0.3f");          // 0.30000001192092896	= 0.3f
    
    //0.10000000149011612	= 0.1f
    //0.20000000298023224	= 0.2f
    //0.30000001192092896	= 0.3f
  • 反着写是因为这样小数位的比较更清晰
    • 0.10000000149011612 ≈ 0.1f
    • 0.20000000298023224 ≈ 0.2f
    • 0.30000001192092896 ≈ 0.3f
    • 0.30000000447034836 ≈ 0.1f + 0.2f
  • 对比结果分析如下
    • float使用==比较的时候,截取6-7位,导致 System.out.println(0.1f + 0.2f == 0.3f); //输出true
    • 若不写后缀,默认double,截取16位,导致System.out.println(0.1 + 0.2 == 0.3); //输出false

5.3 案例2结果分析

  • 思考一下为何1.4f转化为double数据后,结果比原值要小。
    • System.out.println((double)f); //1.399999976158142
    浮点数	1.4
    ========开始解析每一位的含义========
    00111111101100110011001100110011
    0011-1111-1011-0011-0011-0011-0011-0011
    符号位值(1位):[0]为正数
    指数部分(8位):[01111111]	-> 转化十进制数:[127]
    有效数字(23位):[01100110011001100110011]
    =======还原float的真实存储值=======
    阶码偏移量=指数部分的十进制数值[127]- [2^(n-1)-1 (即127)]=0
    指数值=2^阶码偏移量[0]=1.0
    整数部分(小数点左边的部分)即整数位(基础值)=0.0
    尾数23位实际为1.xxx,尾数为有效数字加上1.0
    所以有效位数的二进制表示为[1.01100110011001100110011] -> 有效位数10进制数的值1.399999976158142
    (有效位数10进制数的值1.399999976158142)*(指数值1.0)=小数有效值:1.399999976158142
    1.4为正数,小数有效值[1.399999976158142]加上整数部分[0.0],最终结果:1.399999976158142
    1.399999976158142	= 1.4f
  • 思考一下为何1.5f转化为double数据后,结果和原值一样。
    • System.out.println((double) f1); //1.5
    浮点数	1.5
    ========开始解析每一位的含义========
    00111111110000000000000000000000
    0011-1111-1100-0000-0000-0000-0000-0000
    符号位值(1位):[0]为正数
    指数部分(8位):[01111111]	-> 转化十进制数:[127]
    有效数字(23位):[10000000000000000000000]
    =======还原float的真实存储值=======
    阶码偏移量=指数部分的十进制数值[127]- [2^(n-1)-1 (即127)]=0
    指数值=2^阶码偏移量[0]=1.0
    整数部分(小数点左边的部分)即整数位(基础值)=0.0
    尾数23位实际为1.xxx,尾数为有效数字加上1.0
    所以有效位数的二进制表示为[1.10000000000000000000000] -> 有效位数10进制数的值1.5
    (有效位数10进制数的值1.5)*(指数值1.0)=小数有效值:1.5
    1.5为正数,小数有效值[1.5]加上整数部分[0.0],最终结果:1.5
    1.5	= 1.5f

06.BigDecimal设计

6.1 BigDecimal介绍

  • 为了避免精度丢失,可以使用BigDecimal类进行精确的十进制计算。
    • BigDecimal提供了高精度的十进制运算,但需要注意它的性能相对较低。

6.2 BigDecimal设计思想

6.3 BigDecimal原理

更多内容

  • GitHub:https://github.com/yangchong211
  • 博客:https://juejin.cn/user/1978776659695784
  • 博客汇总:https://github.com/yangchong211/YCBlogs
  • 设计模式专栏:https://github.com/yangchong211/YCDesignBlog
  • Java进阶专栏:https://github.com/yangchong211/YCJavaBlog

学习

  • Java学习之double类型数据比较
    • https://blog.csdn.net/u012235651/article/details/73199464
  • 深入理解Java浮点数机制【详析】
    • https://blog.csdn.net/Return_head/article/details/88623060
  • 瑞_Java中浮点数精度误差产生原因_浮点数底层存储结构(详细附代码)
    • https://blog.csdn.net/weixin_45988482/article/details/135379167
  • 16 | 浮点数和定点数(下):深入理解浮点数到底有什么用?
    • https://blog.csdn.net/qq_37756660/article/details/135913868
  • BigDecimal的原理
    • https://blog.csdn.net/weixin_48303059/article/details/129303632
贡献者: yangchong211
上一篇
1.1String深入理解原理
下一篇
1.3数据装箱和拆箱原理