从01开始 从01开始
首页
  • 计算机科学导论
  • 数字电路
  • 计算机组成原理

    • 计算机组成原理-北大网课
  • 操作系统
  • Linux
  • Docker
  • 计算机网络
  • 计算机常识
  • Git
  • JavaSE
  • Java高级
  • JavaEE

    • Ant
    • Maven
    • Log4j
    • Junit
    • JDBC
    • XML-JSON
  • JavaWeb

    • 服务器软件
    • Servlet
  • Spring
  • 主流框架

    • Redis
    • Mybatis
    • Lucene
    • Elasticsearch
    • RabbitMQ
    • MyCat
    • Lombok
  • SpringMVC
  • SpringBoot
  • 学习网课的心得
  • 输入法
  • 节假日TodoList
  • 其他
  • 关于本站
  • 网站日记
  • 友人帐
  • 如何搭建一个博客
GitHub (opens new window)

peterjxl

人生如逆旅,我亦是行人
首页
  • 计算机科学导论
  • 数字电路
  • 计算机组成原理

    • 计算机组成原理-北大网课
  • 操作系统
  • Linux
  • Docker
  • 计算机网络
  • 计算机常识
  • Git
  • JavaSE
  • Java高级
  • JavaEE

    • Ant
    • Maven
    • Log4j
    • Junit
    • JDBC
    • XML-JSON
  • JavaWeb

    • 服务器软件
    • Servlet
  • Spring
  • 主流框架

    • Redis
    • Mybatis
    • Lucene
    • Elasticsearch
    • RabbitMQ
    • MyCat
    • Lombok
  • SpringMVC
  • SpringBoot
  • 学习网课的心得
  • 输入法
  • 节假日TodoList
  • 其他
  • 关于本站
  • 网站日记
  • 友人帐
  • 如何搭建一个博客
GitHub (opens new window)
  • JavaSE

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

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

    • Java核心类

    • IO

    • Java与时间

    • 异常处理

    • 哈希和加密算法

    • Java8新特性

    • 网络编程

    • Java
  • JavaSenior

  • JavaEE

  • JavaWeb

  • Spring

  • 主流框架

  • SpringMVC

  • SpringBoot

  • Java并发

  • Java源码

  • JVM

  • 韩顺平

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

Java中的浮点数

# Java中的浮点数

‍

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

观察下面的代码:

public class DataTypeDouble {
  public static void main(String[] args) {
    double d = 0.3;
    double d2 = 0.1;
    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
14

‍

编译和运行的结果:

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

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

为什么会出现这样的现象呢?这得涉及到二进制和浮点数在计算机中如何存储的知识点了,参考《数字电路》:数字与进制 (opens new window)。我们这里只简单讲一下。

# 二进制与小数

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

那么小数,如何转为二进制呢?:采用"乘 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。

‍

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

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

‍

‍

# 不准确带来的问题

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

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

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

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

但Largent使用了假名,包括卡通人物的名字,假的地址和社会保障号码,因此了违反了邮件、银行和电信欺骗法律。Largent目前已被保释。 千万别在中国尝试,这是要判无期徒刑的。

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

因此,在开发过程中,绝对不能使用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)   ‍

在GitHub上编辑此页 (opens new window)
上次更新: 2022/12/17 16:33:37
安装Java
Java多版本配置

← 安装Java Java多版本配置→

Theme by Vdoing | Copyright © 2022-2023 粤ICP备2022067627号-1 粤公网安备 44011302003646号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式