从 01 开始 从 01 开始
首页
  • 📚 计算机基础

    • 计算机简史
    • 数字电路
    • 计算机组成原理
    • 操作系统
    • Linux
    • 计算机网络
    • 数据库
    • 编程工具
    • 装机
  • 🎨 前端

    • Node
  • JavaSE
  • Java 高级
  • JavaEE

    • 构建、依赖管理
    • Ant
    • Maven
    • 日志框架
    • Junit
    • JDBC
    • XML-JSON
  • JavaWeb

    • 服务器软件
    • 环境管理和配置管理-科普篇
    • Servlet
  • Spring

    • Spring基础
  • 主流框架

    • Redis
    • Mybatis
    • Lucene
    • Elasticsearch
    • RabbitMQ
    • MyCat
    • Lombok
  • SpringMVC

    • SpringMVC 基础
  • SpringBoot

    • SpringBoot 基础
  • Windows 使用技巧
  • 手机相关技巧
  • 最全面的输入法教程
  • 最全面的浏览器教程
  • Office
  • 图片类工具
  • 效率类工具
  • 最全面的 RSS 教程
  • 码字工具
  • 各大平台
  • 校招
  • 五险一金
  • 职场规划
  • 关于离职
  • 杂谈
  • 自媒体
  • 📖 读书

    • 读书工具
    • 走进科学
  • 🌍 英语

    • 从零开始学英语
    • 英语兔的相关视频
    • Larry 想做技术大佬的相关视频
  • 🏛️ 政治

    • 反腐
    • GFW
    • 404 内容
    • 审查与自我审查
    • 互联网
    • 战争
    • 读书笔记
  • 💰 经济

    • 关于税
    • 理财
  • 💪 健身

    • 睡眠
    • 皮肤
    • 口腔健康
    • 学会呼吸
    • 健身日志
  • 🏠 其他

    • 驾驶技能
    • 租房与买房
    • 厨艺
  • 电影

    • 电影推荐
  • 电视剧
  • 漫画

    • 漫画软件
    • 漫画推荐
  • 游戏

    • Steam
    • 三国杀
    • 求生之路
  • 小说
  • 关于本站
  • 关于博主
  • 打赏
  • 网站动态
  • 友人帐
  • 从零开始搭建博客
  • 搭建邮件服务器
  • 本站分享
  • 🌈 生活

    • 2022
    • 2023
    • 2024
    • 2025
  • 📇 文章索引

    • 文章分类
    • 文章归档

晓林

程序猿,自由职业者,博主,英语爱好者,健身达人
首页
  • 📚 计算机基础

    • 计算机简史
    • 数字电路
    • 计算机组成原理
    • 操作系统
    • Linux
    • 计算机网络
    • 数据库
    • 编程工具
    • 装机
  • 🎨 前端

    • Node
  • JavaSE
  • Java 高级
  • JavaEE

    • 构建、依赖管理
    • Ant
    • Maven
    • 日志框架
    • Junit
    • JDBC
    • XML-JSON
  • JavaWeb

    • 服务器软件
    • 环境管理和配置管理-科普篇
    • Servlet
  • Spring

    • Spring基础
  • 主流框架

    • Redis
    • Mybatis
    • Lucene
    • Elasticsearch
    • RabbitMQ
    • MyCat
    • Lombok
  • SpringMVC

    • SpringMVC 基础
  • SpringBoot

    • SpringBoot 基础
  • Windows 使用技巧
  • 手机相关技巧
  • 最全面的输入法教程
  • 最全面的浏览器教程
  • Office
  • 图片类工具
  • 效率类工具
  • 最全面的 RSS 教程
  • 码字工具
  • 各大平台
  • 校招
  • 五险一金
  • 职场规划
  • 关于离职
  • 杂谈
  • 自媒体
  • 📖 读书

    • 读书工具
    • 走进科学
  • 🌍 英语

    • 从零开始学英语
    • 英语兔的相关视频
    • Larry 想做技术大佬的相关视频
  • 🏛️ 政治

    • 反腐
    • GFW
    • 404 内容
    • 审查与自我审查
    • 互联网
    • 战争
    • 读书笔记
  • 💰 经济

    • 关于税
    • 理财
  • 💪 健身

    • 睡眠
    • 皮肤
    • 口腔健康
    • 学会呼吸
    • 健身日志
  • 🏠 其他

    • 驾驶技能
    • 租房与买房
    • 厨艺
  • 电影

    • 电影推荐
  • 电视剧
  • 漫画

    • 漫画软件
    • 漫画推荐
  • 游戏

    • Steam
    • 三国杀
    • 求生之路
  • 小说
  • 关于本站
  • 关于博主
  • 打赏
  • 网站动态
  • 友人帐
  • 从零开始搭建博客
  • 搭建邮件服务器
  • 本站分享
  • 🌈 生活

    • 2022
    • 2023
    • 2024
    • 2025
  • 📇 文章索引

    • 文章分类
    • 文章归档
  • JavaSE

    • 我的 Java 学习路线
    • 安装 Java
    • Java 数据类型

      • Java 中的浮点数
        • 二进制与小数
        • 不准确带来的问题
        • 解决方法
        • Java 和 IEEE 754 标准
        • 推荐阅读
    • Java 多版本配置
    • 面向对象

    • Java 核心类

    • IO

    • Java 与时间

    • 异常处理

    • 哈希和加密算法

    • Java8 新特性

    • 网络编程

  • JavaSenior

  • JavaEE

  • JavaWeb

  • Spring

  • 主流框架

  • SpringMVC

  • SpringBoot

  • Java
  • JavaSE
  • Java 数据类型
2022-12-16
目录

Java 中的浮点数

# Java 中的浮点数

在 Java 中,double 和 float 的计算是不精确的。

观察下面的代码:

public class DataTypeDouble {
  public static void main(String[] args) {
    double d = 0.1;
    double d2 = 0.2;
    double d3 = d + d2;
    System.out.println(d3);

    float f = 0.3f;
    float f2 = 0.1f;
    float f3 = f - f2;
    System.out.println(f3);
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

编译和运行的结果:

javac DataTypeDouble.java
java DataTypeDouble  
0.30000000000000004
0.20000002
1
2
3
4

不单单是 Java,几乎所有现代的编程语言都会遇到上述问题,包括 JavaScript、Ruby、Python、Swift 和 Go 等等。你可以在 0.30000000000000004.com/ (opens new window) 中找到常见的编程语言在计算 0.1 + 0.2 的结果

为什么会出现这样的现象呢?这得涉及到二进制和浮点数在计算机中如何存储的知识点了,我们这里简单讲一下。

# 二进制与小数

计算机中使用的是二进制,但我们如果想要存储十进制的数,怎么办呢?这就得用到二进制和十进制数的转换。

那么小数,如何转为二进制呢?采用"乘 2 取整,顺序排列"法:

  1. 用 2 乘十进制小数,可以得到积,将积的整数部分取出
  2. 再用 2 乘剩下的小数部分,又得到一个积,再将积的整数部分取出
  3. 重复操作,直到积中的小数部分为零,此时 0 或 1 为二进制的最后一位,或者达到所要求的精度为止

例如将 0.125 转换为二进制:

  • 0.125 * 2 = 0.25 ------0
  • 0.25 * 2 = 0.5 ------0
  • 0.5 * 2 = 1.0 ------1

当小数部分为 0 就可以停止乘 2 了,然后正序排序就构成了二进制的小数部分:0.001

但是,有些小数转为二进制数是无限循环的(就好比 1/3 也是无限循环的 0.33333.......),我们以 0.1 为例:

  • 0.1×2=0.2 ------ 0
  • 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.7,转化为二进制小数是:0.1011001100110......,循环节是 0110。

结论:不是所有的十进制数都能转化为有限位二进制数的。

那些能分解为以(1/2)n 为单位的十进制小数,才可以转化为有限位数的二进制小数。

如十进制数:13/16=0.8125,它可以是拆成:13/16=1/2+1/4+1/16,或者直接可以看作是 13 个 1/16 所组成。而 1/2,1/4,1/16 这些数都是符合(1/2)n 形式的数。

就是因为有些小数,转为二进制是无限的小数,既然是无限的,计算机中肯定是存储不下来的,所以计算机中就无法很精确的存储浮点数;在运算的过程中,既然用的就是不准确的值了,那么得到的结果也就是不准确的了(只能说近似)。

想了解更多可以参考我写的博客:数字与进制 (opens new window)。

此外,计算机内是采用 IEEE 754 标准存储浮点数的,也可以去查看下该文件。

# 不准确带来的问题

这种不准确,很可能直接造成金钱上的损失。即使是零点零几的误差,累积下来也是巨大的损失。曾在网上看到一个新闻:

国外黑客利用银行漏洞 一次盗一美分 (opens new window)

据国外媒体报道,一位“黑客”利用银行漏洞从 PayPal、Google Checkout 和其它在线支付公司窃取了 5 万多美元,每次只偷几美分。他所利用的漏洞是:银行在开户后一般会向帐号发送小额钱去验证帐户是否有效,数额一般在几美分到几美元左右。

Google Checkout 和 Paypal 也使用相同的方法去检验与在线帐号捆绑的信用卡和借记卡帐号。 根据法庭公文,加利福尼亚人 Michael Largent 用一个自动脚本开了 58,000 个帐号,收集了数以千计的超小额费用,汇入到几个个人银行账户中去。

他从 Google Checkout 服务骗到了 $8,000 以上的现金。银行注意到了这种奇怪的现金流动,和他取得联系,Largent 解释他仔细阅读过相关服务条款,相信自己没做错事,声称需要钱去偿还债务。

但 Largent 使用了假名,包括卡通人物的名字,假的地址和社会保障号码,因此了违反了邮件、银行和电信欺骗法律。Largent 目前已被保释。

因此,在开发过程中,绝对不能使用 double 和 float 来存储和计算金额:老板,用 float 存储金额为什么要扣我工资 - 掘金 (opens new window)

# 解决方法

借用《Effactive Java》书中的一句话,float 和 double 类型设计的主要目标是为了科学计算和工程计算。它们主要用于执行二进制浮点运算,这是为了在广域数值范围上提供较为精确的快速近似计算而精心设计的。

但是,它们没有提供完全精确的计算结果,所以不应该被用于要求精确结果的场合。

在商业计算中往往要求结果精确,解决方案:

  1. 使用 JDK 提供的 BigDecimal:BigDecimal (opens new window)
  2. 将用户输入的浮点数,存储到字符串里,然后自己实现字符串里的数字的加减法,然后输出字符串。例如将有个字符串数组“1.1”,要和另一个字符串数组“1.2”相加,就逐个取出字符串里的内容,转为成整数相加,然后得到结果放到另一个字符串数组里。

# Java 和 IEEE 754 标准

在 Double 中,有这样一个方法:doubleToRawLongBits(double value),可以将 double 所表示的 64 位数用 long 表示出来。

这个方法是原始方法(注意其 native 修饰符,底层不是 java 实现,是 c++ 实现的),官方文档说明如下:

    public static native long doubleToRawLongBits(double value);
1

根据 IEEE 754 浮点“双精度格式”位布局,返回指定浮点值的表示形式,并保留 NaN 值。

位 63(由掩码 0x8000000000000000L 选择的位)表示浮点数的符号。 比特 62-52(由掩码 0x7ff0000000000000L 选择的比特)表示指数。 位 51-0(由掩码 0x000fffffffffffffL 选择的位)表示 0x000fffffffffffffL 的有效数(有时称为尾数)。

如果参数为正无穷大,则结果为 0x7ff0000000000000L 。

如果参数为负无穷大,则结果为 0xfff0000000000000L 。

如果参数为 NaN,则结果为 long 整数,表示实际的 NaN 值。 与 doubleToLongBits 方法不同, doubleToRawLongBits 不会将编码 NaN 的所有位模式折叠为单个“规范”NaN 值。

在所有情况下,结果都是 long 整数,当给定 longBitsToDouble(long) 方法时,将产生与 doubleToRawLongBits 的参数相同的浮点值。

.......

参数:value - 双精度 (double) 浮点数。 返回:表示浮点数的位。

官方英文文档:Double (Java Platform SE 8 ) (opens new window)

举个例子:

public class TestDoubleToRawLongBits {
  public static void main(String[] args) {
    double d  = 1;
    System.out.println(Double.doubleToRawLongBits(d));
  }
}
1
2
3
4
5
6

编译和有运行:

javac TestDoubleToRawLongBits.java
java TestDoubleToRawLongBits  
4607182418800017408
1
2
3

这个结果是怎么来的呢?首先,将 4607182418800017408 转为二进制(可以用 Windows10 的计算器转化):

4607182418800017408
0011111111110000000000000000000000000000000000000000000000000000
1
2

将这串二进制,按照 IEEE 754 的标准,划分成 1 位符号数,11 位阶码和 23 位尾数:

0 01111111111 0000000000000000000000000000000000000000000000000000
1

其中,阶码是用移码表示的,因此实际的值要减去 127,得到的结果是 0.

也就是说,2 的 0 次方,也就是 1,就是我们前面定义的变量 d: double d = 1;

但是,这是因为刚好整数 1 可以在计算机中存储,如果是 0.1 这种,在计算机里存储就是不准确的值了,得到的结果也很奇怪:

double d2 = 0.1;
System.out.println(Double.doubleToRawLongBits(d2));
1
2

输出是:4591870180066957722,转为二进制是:

0011111110111001100110011001100110011001100110011001100110011010

感兴趣的读者可以自行根据 IEEE 754 标准计算其所代表的值是什么。

# 推荐阅读

为什么 0.1 + 0.2 = 0.300000004 · Why's THE Design? - 掘金 (opens new window)

上次更新: 2025/6/3 09:31:54
安装 Java
Java 多版本配置

← 安装 Java Java 多版本配置→

最近更新
01
学点统计学:轻松识破一本正经的胡说八道
06-05
02
2025 年 5 月记
05-31
03
《贫穷的本质》很棒,但可能不适合你
05-27
更多文章>
Theme by Vdoing | Copyright © 2022-2025 | 粤 ICP 备 2022067627 号 -1 | 粤公网安备 44011302003646 号 | 点击查看十年之约
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式